0

My Qt-Application (currently on PySide2 (Qt 5.15.6)) uses QTreeViews and appropriate Models to show big hierarchical structures, some of them really deep and/or containing reference loops. For displaying only nodes that match a given QRegExp I derived QSortFilterProxyModel and reimplemented filterAcceptsRow to decide if a node should be displayed. In case a node doesn't match the RegExp, I walk down recursively to check if any of its' descendants match... more or less that's shown in the code below.

def filterAcceptsRow( self, row, parent ):
    def _match( index ):
        return regExp.exactMatch( pathOf(index) )

    def _check( *indices ):
        return any( _match(idx) for idx in indices ) or \
               any( _walk(idx)  for idx in indices )
    
    def _walk( index ):# . o O (how to determine that break was requested?)
        if _breakRequested(): return False
        else                : return any( _check(idx) for idx in childrenOf(index) )

    return _check( indexOf( row, col, parent )

This works usually quite well and (with some optimization) fast. However, in case of a deep tree structure and a careless user input the recursive descent might take a while what is leading to a blocked GUI.

So long story short: Is there any way of aborting an intensive GUI update (like here filtering nodes) by e.g. pressing ESC?
Here some thoughts and/or what I tried so far...

  • Since filterAcceptsRow is called from Qt for updating the GUI I guess I can't really shift it to some worker thread, right? Well, I tried and it ended fatally ... :-S
  • In the dummy code above I put a call to _breakRequested() where I would ask for a break request. But how would such function look like?
  • Since we're right in processing some Event that led to our filtering the event loop is blocked and can't register any key strokes, right?
  • Calling processEvents() here (in order to detect key strokes) would mix the processing order of events. According to the try I gave it, bad idea :-(.

I searched and tried a lot of different approaches, without luck. Am I really the only one who wants to escape from a blocked GUI while the blocking part can't be shifted to another thread?

  • Basically I don't need to (and must not) process the events at that point (_breakRequested()). I would be fine with an event look ahead by sneaking along the event queue and looking for an according key event. However this doesn't seem possible in Qt before version 6.
  • Even if it was possible, is Qt registering Events (adding them to the event queue) between subsequent calls to filterAcceptsRow? Is there something like queueEvents() without processing them?
  • While the GUI is busy, is it possible to open another (modal) Widget with its' own thread and event loop listening for key strokes?

Sorry if this all is just bullshit but as you see, I got a bit stuck ... can somebody show me the light, please?

Zappotek
  • 344
  • 2
  • 5
  • You cannot directly use threading because the model is attached to a view, they must exist on the same (main) thread and direct "queries" to the model must be in that same thread. Also, remember that UI elements can only be accessed and **created** in the main thread, so you cannot do what you asked in your last question. Long story short, there's no easy or direct way to stop the filtering based on user events. But, if the issue is primarily caused by "careless user input", then the problem is there: maybe you should sanitize or validate the input before applying the filter. – musicamante Apr 24 '23 at 17:02
  • @musicamante: Thanks for your thoughts and confirming my experiences. Well, sanitizing the user input before applying it is always a good idea and in fact, I use a validator to ensure valid expressions. However, I just can't and don't want to prevent a user from searching something that exists deeply buried in the tree, e.g. by `.* node name .*`. And filtering for such expression might really take several minutes ... :-( – Zappotek Apr 25 '23 at 10:02
  • 2
    There is another possibility. I assume that you're using a QAbstractItemModel with custom `object` subclasses for nodes. You could implement an internal search that walks into the internal structure (not passing through the Qt model) in a separate thread, and, whenever a query is matched, emits a signal with the "path" of the result, then append those results to an internal data object (maybe a set?) that includes all the nodes required to show that path (by getting the row and parent), then invalidate the filter and check for those paths in `filterAcceptsRow()`. – musicamante Apr 25 '23 at 19:14

1 Answers1

0

I'm not sure about your use case, but for blocked code segments I would generally try moving the computationally expensive task to a QThread so the mainwindow is still usable and then use for example a hotkey to send an "abort-signal" into the threaded object if needed

(see Background thread with QThread in PyQt for different methods)

and: https://realpython.com/python-pyqt-qthread/


Edit: "ugly approach" using QShortcut:

Events do not necessarily propagate among all widgets, if a widget consumes it then it will no longer propagate to the parent. In the case of the keyboard events will only be consumed first by the widget that has the focus, in your case QWebEngineView consumes them before and prevents it from being projected in other widgets. If you want to hear events from the keyboard of a window then you must use the QShortcuts

since you made a custom class already, you could add something like:

from PyQt5 import QtCore, QtGui, QtWidgets
class YourCustomClass(QSortFilterProxyModel):
    def __init__(self, parent=None):
        super(YourCustomClass, self).__init__(parent)
        self.flag = True
        QtWidgets.QShortcut(
            QtGui.QKeySequence("Escape"), self, activated=self.on_Escape
        )
    @QtCore.pyqtSlot()
    def on_Escape(self):
        self.flag = False

    def filterAcceptsRow( self, row, parent ):
        if self.flag:
            # ...
        else: return

im not sure if this will also block the event loop, but seems worth a try.

btw: you could also try limiting the maximum recursion depth for a user input.

gorschel
  • 26
  • 8
  • That's not feasible, at least not easily nor directly with the model (and the OP already tried it), because the filter function is always called internally (and recursively) by Qt and must always return as soon as possible, since it is executed in the main thread. – musicamante Apr 24 '23 at 18:20
  • OP didn't indicate whether builtin threading or QThread was used, and i think it would be feasible to hold the model/list as a independent object. After finished computation in a background task, use setModel(filteredmodel) to update just the filtered list to the treeview. another, really ugly approach would be to to check for your ESC key in every recursive call (if you really have no possibility of "updating" the widget from a signal) – gorschel Apr 25 '23 at 09:31
  • I tried using a QThread-based worker, similar to the example you linked. But I think there are more problems with this, not just the most prominent one, synchronizing the Model sufficiently for any type of data access. Furthermore, as *musicamente* said earlier, Qt calls the filter function internally and recursively - I think, shifting the search to another thread[s?] and synchronizing the result such that Qt is fine with it would introduce a bunch of new problems to solve. Considering that, I'd really prefer letting the user abort the operation and rethink his/her filter expression ... – Zappotek Apr 25 '23 at 10:38
  • This brings me back to giving a try to the "ugly approach" you mentioned, checking the ESC key ... But how would you do that with the event loop blocked? – Zappotek Apr 25 '23 at 10:41
  • Thanks for your example. I tried it (*1) and as almost expected, also these events do not arrive before filtering is finished. So for the moment, it seems like limiting the recursion depth is indeed my only way out ... :-| By the way: QKeySequences need a widget as a parent - and do not accept passing the ProxyModel. – Zappotek Apr 25 '23 at 18:19
  • @gorschel That would make the proxy quite ineffective: in order to make the model work in another thread, it must be *moved* to the thread, along with the source model, meaning that it must be detached from the view and then reattached by moving its thread again. Also, the computation is quite heavy, and since Python doesn't allow concurrency, the result would be probably the same, if not worst: calling `QApplication.processEvents()` from another thread is ineffective, and then it would have to release control periodically, making the computation longer and the whole approach cumbersome. – musicamante Apr 25 '23 at 19:06