Thursday, April 17, 2014

What's Happening?

The last couple of days have been a long digression into the world of QTest and QEvents. It was motivated by my desire to make an automated unit test particularly of the context menu I've added to the editview.

The motivation for a context menu, like so many changed features of PPQT2, is that there can be multiple books open at once. In V1, there was only one book and accordingly only one:

  • Primary spelling dictionary
  • File of "scannos" (common OCR errors like "arid")
  • Choice of whether or not to highlight spelling errors
  • Choice of whether or not to highlight scannos

But now these choices have to be individualized per book. In V1, there could be a File menu action, "Choose scanno file..." but in V2, if that action was in the File menu, there'd have to be a convention about which of the (possibly several) open books those scannos should apply to. The one with the keyboard focus? Suppose the keyboard focus is in the Help panel? Similarly for the V1 View > Choose Dictionary menu item. And for the View > Highlight Scannos/Spelling toggles. All these choices need to be clearly associated to a single book. Hence, a context menu in the editor panel with four actions. When you have to right-click on the editor in order to choose a scanno file, you know where those scannos will be applied.

The little context menu is in and working, at least to casual testing. But I'm trying to do this shit right; and that means, an automated unit test. And that meant, I presumed, using QTest's mouse actions to simulate a control-click on the edit widget.

So I wrote up a test case that went in part like this:

ev = the_book.editv.Editor # ref to QPlainTextEdit widget
ev_w = ev.width()
ev_h = ev.height()
ev_m = QPoint(ev_w/2,ev_h/2) # the middle
QTest.mouseClick(ev, Qt.LeftButton, Qt.CtlModifier, ev_m, 1000)

...and, it didn't work. Nothing. I tried all sorts of mouse actions using both QTest's methods (mouseClick, mouseDblClick, mousePress, mouseRelease) and actually composing my own QMouseEvent objects and pushing them in with QApplication.postEvent(). Fast forward through about six hours of fiddle-faddling over three days. Sometimes I could get a double-click to work and sometimes not. Mouse presses or clicks with any button and modifier—nada. zip.

Now as it happens, I have an "event filter" on the editor. This is because I want to handle certain keystrokes, as described previously. But the edit widget is created by code generated from Qt Designer. That means it can only be a standard QPlainTextEditor. The normal way to intercept keystrokes is to subclass a widget and override its keyPressEvent() method. Don't think there's a way to get Qt Designer to plug in a custom subclass of a widget type.

However there's a way that you can install an "event filter" on any widget. That directs all events for that widget through the filter function. If it handles the event it returns True; if not, it returns False and the event is presented to the widget in the normal way. So editview puts a filter on events to the edit widget, picks off just the keyEvents it wants, and passes the rest on.

So I took advantage of this to just print out all the events passing through the edit widget so I could find out just what the heck mouse events it was getting when I clicked to bring up the context menu.

Surprise! It doesn't get any!

The Qt docs would have one believe that as a mouse moves over and clicks or drags on a widget, there's a constant flow of QMouseEvent objects to it. Nope. Not on my Macbook, anyway.

There are Enter and Leave events as the mouse pointer comes into and out of the frame of the widget. These aren't mouse events as such. There are lots of other sorts of events like Paint and Tooltip. But there are almost no mouse events posted. What does appear while the mouse is active, on every click and streaming during any drag, is QInputMethodQuery events. During a mouse click or drag, when I'd expect a stream of QMouseEvent postings, all that comes is a stream of QInputMethodQuery.

This peculiar class has only one property, a query with a not very helpful list of values. Of these possible "queries" only one is being sent in my system, the query IMEnabled meaning "The widget accepts input method input". The receiver is supposed to set something from a set of even less-interesting values in the event. Of course, my event filter doesn't see what is being set; it only sees the event on its way in.

Something nefarious is going on here. Perhaps it is only in the Mac OS; perhaps it only affects QTextEdit and derivatives (QTest mouse actions directed to other widgets seem to work). But for the editor, on my macbook, the whole mouse event architecture is effectively being ignored, replaced by something only minimally documented and not amenable to code introspection for unit-testing.

There are also a few InputMethodQueries issued just before any keyPressEvent and I am deeply suspicious that this is related to the inconsistent handling of the Mac keyboard I noted earlier.

That aside, the net from all this investigation is to realize that I don't really need to simulate the mouse at all. All I need to do is to fabricate a QContextMenuEvent with a given position in the middle of the editor. Post that; then use getChildAt that same position to get a reference to the context menu, and then I can send it keystrokes using QTest.

To be tried tomorrow.

No comments: