Friday, April 18, 2014

Context Menus are Hard

Really, context menus are the orphan children of Qt. First, you cannot define one using Qt Designer. So even if all the rest of your UI (including the "real" menus) is designed with the designer, your context menu has to be hand-coded. And second, it appears to be impossible to test one using QTest.

As reported yesterday, it doesn't seem possible to use QTest mouse events to invoke a context menu—at least for a QTextEdit under Mac OS. So today I'm constructing a QContextMenuEvent object my ownself, and passing it to the editor to process. This works, kind of, but not in a useful way for testing. Here's some code.

ev = the_book.editv.Editor
ev_w = ev.width()
ev_h = ev.height()
ev_m = QPoint(ev_w/2,ev_h/2)

cxe = QContextMenuEvent(
    QContextMenuEvent.Mouse,
    ev_m, ev.mapToGlobal(ev_m),
    Qt.NoModifier)

app = QCoreApplication.instance()
app.notify(ev,cxe)
cxm = ev.childAt(ev_w+5,ev_h+5)

QCoreApplication.notify(target,event) "Sends event to receiver... Returns the value that is returned from the receiver's event handler." My supposition was that the context menu would pop up, and I'd get control back and use childAt() to get a reference to that menu, and then I could feed it keystrokes, for example a couple of down-arrows and a return to select the third item.

Well as so often happens, my supposition was wrong. Because I don't get control back! Notice what the documentation says, "the value that is returned from the receiver's event handler"? But that event handler consists entirely of, quote:

    def contextMenuEvent(self, event):
        self.context_menu.exec_(event.globalPos())

It won't be returning until the menu's exec_() method returns! The nice editview window sits there with its context menu open in the middle, and waits for user input. It won't finish until the menu is satisfied with a Return or dismissed with Escape. At which time, there is no longer a menu at any location to be captured or tested.

Another problem I realized yesterday evening was this. Suppose I could get a reference to the menu object and feed it keystrokes. Two of its actions are toggle-able or check-able choices; in order to verify they had worked, I'd have to somehow sample their toggle status. How does one ask, does this menu choice have a check-mark on it?

The other two choices are worse. One invokes a native OS file-selection dialog, which is certainly beyond the reach of QTest or anything based on the Qt event mechanism. So how could my unit-test code browse to a test scanno file and click OK? And then how could it verify that (at least one of) the expected words was now highlighted?

And the other pops up a dialog with a drop-down menu of available dictionary tags. I'd like to have a test that verified that this dialog appeared, that it had the expected number of entries, that the last-chosen dictionary tag was the selected one, that another could be chosen, and so forth. But my test code doesn't get control back while the context menu is up, and it won't end until that dictionary dialog ends, so... bleagh.

1 comment:

Anonymous said...

I had the same problem.
The trick is to use an event filter on qApp.
Install it before emitting QContextMenuEvent. Use qApp->notify to send it.
This call will yield control to the context menu and the exec loop. Your filter will receive all messages that occur. Prepare the filter class to do the automation you need: select an item, or emulate escape to close it. Regardless of what you prepare it for, it must always end with the menu closing. Otherwise you will never get control back.
In your filter class, look for Show message type. qobject_cast the object to QMenu. If not NULL, that is your QMenu widget. Ask and record all relevant information: its actions and their properties (display, separator or not, enabled or not, visual rectangle). On the same message schedule a single shot timer with a delay to "do the prepared automation". The delay allows messages to be processed and the menu to actually appear.
On the slot that was given to the single-shot timer, do your automation: select an item using QMouseEvents and POST them. A successful scheduling should always mean the QMenu closes. So plan accordingly.
If scheduling is not successful for any reason (the prepared automation could not be scheduled because the item could not be found in the menu, etc.) use a single-shot timer again to schedule an emulation of "escape": QKeyEvent (Press, Release) using Key_Escape. This ensures that always your menu will close and control is given back to automation.
That is all. Hopefully, if others hit a wall on this problem, the above pseudo-solution will help them get over the wall.