PDA

View Full Version : filtering a TreeView from buttom to top



soul_rebel
10th March 2008, 16:16
What I want to do is rather simple:
When filtering my QTreeView, I want to filter on the children first and hide the parent if - and only if - it has no more (visible) children.

I subclassed QSortFilterProxyModel and got to the point where it filters correctly on the children and never hides the parent.
The last is accomplished by(in filterAcceptsRow())
/// if toplevel
if (sourceParent == ((QStandardItemModel*)sourceModel())->indexFromItem(
((QStandardItemModel*)sourceModel())->invisibleRootItem()) )
return true;
The Problem is I cant check if the item hasChildren() because at the point where an item is processed by filterAcceptsRow it never has any children (in the filtered Model).
What I tried first was to check after the last child of a parent is processed whether its parent (in the filter model) has any children, and if not remove it.
That doesnt work because filterAcceptsRow() has to be const. I tried all sorts of work-arounds with signals and slots but havent succeded.

What should I do? It seems to me the desired behaviour is rather normal. At least it was default for filtering KListViews in KDE3.

Thanks for your help!

wysota
11th March 2008, 13:29
First of all you shouldn't cast the source model to QStandardItemModel...

I think you'll need to compute the visibility of child items when deciding if a particular item is to be shown or not. You can cache the result somewhere (most probably using QModelIndex::internalPointer()), so that you can reuse it when making decisions about children.

soul_rebel
5th April 2008, 18:07
First of all you shouldn't cast the source model to QStandardItemModel...

I think you'll need to compute the visibility of child items when deciding if a particular item is to be shown or not. Isn't filterAcceptsRow() that place?
You can cache the result somewhere (most probably using QModelIndex::internalPointer()), so that you can reuse it when making decisions about children.
Well I can't write any data from filterAcceptsRow() because thats const :(

Seems to me like the QSortFilterProxyModel is really incapable of doing what I want it to... Maybe I should just write my own subclassing QStandardItemModel, then I would have no porlbems using item-logic on the filterview's items and wouldn't have to cast the sourcemodel...
Or would you recommend against that?

Thanks for you help and sorry for the long time periods between answering (sadly I dont have mor time for developing free software).

wysota
5th April 2008, 21:14
Isn't filterAcceptsRow() that place?
Theoretically yes it is. The point is you have to do that when the proxy asks for visibility of the top most item. Of course you can do that elsewhere (like in a slot connected to dataChanged and a bunch of other signals), and simply return the decision in filterAcceptsRow(). That would solve your other problem.


Well I can't write any data from filterAcceptsRow() because thats const :(
Sure you can :) Either read about a "mutable" keyword or store data in an external object to fool the compiler.


Seems to me like the QSortFilterProxyModel is really incapable of doing what I want it to... Maybe I should just write my own subclassing QStandardItemModel, then I would have no porlbems using item-logic on the filterview's items and wouldn't have to cast the sourcemodel...
The "problem" is not with the filter, but with the architecture of the tree itself. When an item is shown there is no point in asking if its (grand)*children should be visible or not. That's why the view only asks about child items when it needs to.

soul_rebel
22nd April 2008, 00:02
Thanks for the help. I've read up on mutable and tried all sorts of dirty workarounds that couldnt do it....

Now I am (back) to a more sensible approach I think: I set all top-level-items visible in filterAcceptsRow() and when rowsRemoved() is emitted I check whether a top-level remains empty to remove it then.

Unfortunately that doesnt work either... here's the code: (the slot is connected to rowsRemoved(..):

void rowsWereRemoved(const QModelIndex & parent, int start, int end)
{
if (parent != QModelIndex()) /// ! toplevel is being removed
if (!hasChildren(parent))
removeRow(parent.row(), parent.parent());
}

It causes a segfault on filtering immediately leaving completely unusable dumps....
So far all my attempts segfault as soon as a row is removed :( Is there something fundamentally flawed with my syntax?

Thanks for your help!!

wysota
22nd April 2008, 00:17
Calling removeRow() from within a proxy causes the call to be forwarded to the source model and I doubt this is what you want. Why don't you do what I suggested - calculate the visibility of the whole tree and cache it in the proxy so that all you do within filterAcceptsRow() and family is return the cached value.

soul_rebel
22nd April 2008, 01:38
Hmmm this is driving me nuts.... here are my attempts at "prefiltering"

I have a member
QMap <qint64, bool> visibleTable;
that saves internalPointer<->visiblity;

This is the extra function
bool QBrowseViewFilterModel::myFilterAcceptsRow(int sourceRow,
const QModelIndex &sourceParent)
{

QModelIndex index0 = sourceModel()->index(sourceRow, 0, sourceParent);
bool show = false;

if (sourceModel()->hasChildren(index0))
{
for (int i = 0; i < sourceModel()->rowCount(index0); ++i)
if (myFilterAcceptsRow(i, index0))
show = true; /// if any child is visible keep this visible
} else
{
QModelIndex index1 = sourceModel()->index(sourceRow, 1, sourceParent);
QModelIndex index2 = sourceModel()->index(sourceRow, 2, sourceParent);

show = true;

if (!sourceModel()->data(index0).toString().contains(filterRegExp()))
show = false;

}

visibleTable.insert(index0.internalId(), show);

return show;
} the original filterAcceptsRow :

bool QBrowseViewFilterModel::filterAcceptsRow(int sourceRow,
const QModelIndex &sourceParent) const
{
QModelIndex index0 = sourceModel()->index(sourceRow, 0, sourceParent);
return visibleTable[index0.internalId()];
}

And I also overwrote invalidate and reset to actually start the recursion on the top-level-items:
void QBrowseViewFilterModel::reset()
{
visibleTable.clear();

for (int i = 0; i < sourceModel()->rowCount(); ++i)
myFilterAcceptsRow(i);

this->QSortFilterProxyModel::reset();
}

void QBrowseViewFilterModel::invalidate()
{
visibleTable.clear();

for (int i = 0; i < sourceModel()->rowCount(); ++i)
myFilterAcceptsRow(i);

this->QSortFilterProxyModel::invalidate();
}

But now the table¹ is empty :crying:

... I hope I am not doing anything stupid there right now.... it is getting kind of late....

Thanks for not giving up :)

edit: ¹ I meant the view.

wysota
22nd April 2008, 03:54
I'd do it differently. Connect to all signals telling you about changes in the source model. In a slot connected to the signal simply recompute the visibility of the changed subtree. Make sure your proxy is not used for sorting, so that it is purely a filtering proxy and you can be sure the layout of items won't change frequently. If you need sorting, apply another proxy over yours to provide the sorting layer. Your proxy shouldn't check if the source model has children but instead it should only determine visibility of its own items based on its own children (at least that's how I understand your use case). You should have a method that will be called recursively on subtrees which will compute visibility of a subtree and based on its result the visibility of the parent can be determined. It's not an easy task so it might prove easier if you do it outside your codebase on a clean and simple model with a simpler to make decision.

soul_rebel
23rd April 2008, 00:52
Man! After doing the craziest things you can do with qt, after fiddling around with beginRemoveRows, after like 20 different half-working approaches I can up with maybe the most simple solution, requiring no compiler-fooling, no extra data structures, no workarounds, no nothings:
I simply start into a recursion while in filterAcceptsRow since that works on the source's items anyways!
Here's the code, in case anyone ever goes through the same hell as me ;)


bool QBrowseViewFilterModel::filterAcceptsRow(int sourceRow,
const QModelIndex &sourceParent) const
{
QModelIndex col0 = sourceModel()->index(sourceRow, 0, sourceParent);

/// do bottom to top filtering
if (sourceModel()->hasChildren(col0))
{
for (int i=0; i < sourceModel()->rowCount(col0); ++i)
if (filterAcceptsRow(i, col0))
return true;
return false;
}

return sourceModel()->data(col0).toString().contains(filterRegExp()));
}

Jeeze, I still cant believe it :o

Thanks for reading my rambling and thanks for your time altogether!

p.s.: I don't even need to do any dirty casting....

wysota
23rd April 2008, 01:08
Hmm.. isn't that what I suggested in the first place?


I think you'll need to compute the visibility of child items when deciding if a particular item is to be shown or not. You can cache the result somewhere (most probably using QModelIndex::internalPointer()), so that you can reuse it when making decisions about children.

Remember to cache the value or it will get reeeeeaaaaaaal slow when the depth of the tree increases.

soul_rebel
23rd April 2008, 01:56
Hmm.. isn't that what I suggested in the first place? Well, kind of ;)
From your latter posts I somehow got the impression it could not *just* be done in that function. (but that could have been my

Remember to cache the value or it will get reeeeeaaaaaaal slow when the depth of the tree increases.
Well, I thought about that. It sure would be the cleaner implementation but in my app the tree has exactly depth 2, always.
So storing the visibilty-values in an extra list and iterating trough that everytime befor applying the filter won't really help much I think. And filtering is near instant right now(only avery small lagg), although there are nearly 20k items altogether in the list.

wysota
23rd April 2008, 08:15
Well, I thought about that. It sure would be the cleaner implementation but in my app the tree has exactly depth 2, always.
Hmm... you could have said that earlier, you know... we could have solved the problem in 3 posts then :)