[Development] QEvent::accept() vs. the newer event delivery algorithms in Qt Quick; remaining API issues; etc.

Shawn Rutledge shawn.rutledge at qt.io
Thu Oct 8 13:26:55 CEST 2020


If you subclass QQuickItem and start handling events, it becomes clear that QEvent::accept() has always meant two things in legacy Qt Quick: 1) stop propagation and 2) grab the mouse.  (Does this idea bother you yet?)

The item is basically saying “the buck stops here”: it’s so very sure that no other item in the scene could possibly care about that event.  Because it has total scene awareness.  (Does that sound arrogant?)

If you place anything that reacts to press (like a Controls Button, or a QML component that acts like one) into a Flickable, it will be “on top” of the z stack during event delivery, so it gets the press event first.  It also wants to see the release (that’s how a click is detected).  In classic Qt Quick, it must grab the mouse, otherwise it gets no more events after the press.  And the original way was to accept the event.  But then after you press, you still want to be able to drag, so as to start flicking (which implies the button should no longer be pressed).  But the Flickable won’t see the event because it’s underneath this top item that is stopping propagation.  So this item, that is acting like it’s so sure that “the buck stops here”, has to be preempted by the flickable.  But how?  Well, that’s why we have  virtual bool  QQuickItem::childMouseEventFilter(QQuickItem *item, QEvent *event) as public API in QQuickItem… so that any item can filter all the mouse events that its children receive.  (Isn’t the signature beautiful? It says it filters mouse events but takes a QEvent* as an argument.  Maybe you can guess why.)  But wait, how can it get a grab or take over a grab if the only normal way to do that is to accept the event?  Well we also have void QQuickItem::grabMouse().  (Because there will only ever be one mouse, no matter how many mice you plug in, and no matter how many fingers you touch the screen with.  It doesn’t matter what’s going on at this moment - just grab the mouse.  Right now.  How hard can it be?)

Now how will we handle multi-touch?  What if one or more fingers is pressing inside the boundary of one Item, but other fingers are elsewhere?  If you have to be able to accept or reject the entire event, that would amount to a little Button saying “the buck stops here” for all fingers at the same time.  That won’t do!  So of course another ugly hack was introduced a long time ago: during delivery to that button, it should only see the fingers that are inside of its bounds, so that when it accepts that event, it’s clear which fingers are being grabbed.  That way it can see the releases of those touchpoints.  So in Qt 5’s delivery of touch events, for each item that we visit, we heap-allocate a brand-new QTouchEvent, customized just for that one item: it contains only the points that are inside, and each point is localized so that its pos() will give the position inside the item that is receiving it.  Let it see the event, check afterwards whether it got accepted or not, and then destroy it.

Then cases arise where even child-event-filtering is not enough - you need surveillance of all events going to all of those greedy, grabby items in the scene, just to make sure you can take over control of any event at any time.  Well, that’s what QObject event filtering is for.

All the above ought to be obsolete BTW.  (How could it not be?)

Before we could introduce Pointer Handlers, we had to substantially re-architect event delivery, while also keeping all the old hacks working (because Controls depends on them, and so does just about every QQuickItem subclass that handles mouse or touch events).  (So that led to a lot of regressions around 5.7 - 5.10 or so.  We fixed most of what we could. I don’t see how it was possible to move forward without making big changes like that, though.)

Qt Quick is basically trying to detect gestures in a distributed fashion: if you press and release a touchpoint on a button, that’s a click gesture.  If you press, drag and release, it’s a drag gesture, but it’s up to an item or handler to detect that’s what’s going on.  Items and handlers need to have enough autonomy to decide for themselves what the user is trying to do, in that geometric neighborhood where they live, while other users or other fingers might be doing something else in different neighborhoods at the same time.  That idea is very much in conflict with the idea that stopping propagation of events by accepting them could ever be OK.

I think that when a mouse button or a touchpoint is pressed, we need to let that event propagate all the way down, until every item and handler that could possibly be interested has had a chance to see it.  In Qt 5.10 or so, we already added the passive grab concept: a pointer handler can subscribe for updates without having to say “the buck stops here”.  But in order to have a chance to do that, it needs to see the press event in the first place.  Any item that is on top by z-order can deny it the opportunity though, by stopping propagation.  So eventually it seems like we need to end up with an agreement that individual items don’t stop propagation anymore: they need to be humble and cooperative, not arrogant and unilateral.  That opens up the opportunity for some handlers to “do their thing” non-exclusively: e.g. PointHandler can act upon the movements of an individual touchpoint without ever taking sole responsibility.  So it can be used to show feedback as an independent aspect of the UI, regardless what else is going on at the same time; and it does it without the older event filtering mechanisms.

But the exclusive grab still means what grabMouse() always did: the item or handler is pretty sure that it has responsibility for the sequence of QEventPoint movements.  It has recognized a gesture and is acting upon it.  In pointer handlers, usually active() becomes true and the handler takes the exclusive grab of the point(s) within its bounds at the same time.  But it does not preempt passive grabs of other handlers: they still get to watch the movements too.  Conflict can arise when one handler (probably one that had a passive grab until now) decides to steal the exclusive grab from something else, and then we have a whole negotiation mechanism in place to deal with that: both handlers have to agree that it’s OK.

But how can all that keep working when we still have so many old-fashioned item subclasses in use?  Well it’s tricky… we keep fixing those cases one at a time.  So far there always seems to be a way.  The sooner we can deprecate some of the older techniques, the better, IMO.  But one of the nagging questions is still what should QEvent::accept() really mean?  Is it OK to have it mean something different in Qt Quick now?  We are not allowed to change what it means in widgets.  I wish we had time to refactor event delivery to be consistent everywhere, but probably we will never find time for that.

For the use case of in-scene popups (like the dialogs and popups in Controls 2), I still think QQuickItem needs a modal flag.  I’ve had a patch sitting around for years to add that, and I don’t think it would be too hard to get it into 6.0.  I think in the future this should be the main way to stop event propagation: an item that is modal will not let input events go through to any other items behind it in Z order.  This way it becomes declarative (set the flag) rather than imperative (accept each event as it comes).  It would be used sparingly, probably on the background item of the dialog or popup itself, or maybe on the translucent item that is usually shown behind it.  Clicking outside to close the popup has to be handled somehow; I think I had something elegant in mind a couple of years ago.

A prerequisite for cooperative event handling seems to be that we need to rewrite QQuickFlickable some day.  I have had the FakeFlickable.qml manual test in place for a long time now, to give a glimpse of what that could look like.  Flickable is the main user of child-event-filtering, but FakeFlickable gets by without it.  Ideally I would have found time to do this rewrite now, for Qt 6.0.  But here we are… management is insisting that we have to follow the usual release schedule, so there’s no time left for much beyond the QInputEvent refactoring that I’ve been doing, which has taken most of my year so far, aside from some distractions here and there.  It’s very frustrating that it basically means rewrite event delivery in Qt Quick yet again, but again keep all the ugly old hacks working (thus we don’t get to re-simplify it yet: it’s still more complex than it was in early 5.x versions.)  Fixing broken hacks that the tests reveal takes a lot of time, and again I’m not getting much help.

As an intermediate step, maybe QQuickFlickable could at least start handling touch events directly so that it doesn’t have to rely on touch->mouse synthesis anymore.  We’ll see, but time is running out.  But as an intermediate step before that, it could at least learn to replay touch events when you’ve set a pressDelay and you’re using a touchscreen and the item inside knows how to handle a touch event.  There are a lot of bugs we can fix that way.  But captureDelayedPress() is called while filtering: if you are pressing a classic grab-happy item inside, it’s the only way Flickable gets a chance to see the press event.  So again we come to this inelegance that somebody named the virtual function QQuickItem::childMouseEventFilter() and then decided later on that oh duh actually it has to filter touch events too.  And in Qt 5 mouse and touch events were not closely related enough; but I don’t know why it takes QEvent, not at least QInputEvent.  In Qt 6 it could be renamed to childPointerEventFilter() and take a QPointerEvent, but it’s a virtual public function in QQuickItem, which means the world is full of subclasses that override it by now.  I don’t want to go through the whole deprecate-and-duplicate song and dance, because I think the whole filtering concept ought to be obsolete eventually.  Duplicating it would mean having to call both the old and new functions during delivery and letting either one of them do the same things, which would add overhead and complication for now.  But getting rid of it depends on everyone eventually agreeing that it’s OK to redesign QQuickFlickable like I want to do: its children must never stop event propagation, to ensure that the flickable itself gets a chance to see the press, so it can take a passive grab.  And that is risky because of another atrocious design decision that we are stuck with: all item views inherit QQuickFlickable in C++.  It’s not a component, it’s the base class that does everything possible.  I can try to refactor it to internally construct pointer handler instances instead of overriding virtual functions, but with all those inheritance use cases…

BTW: If you ever use Flickable on a touchscreen, you can try FakeFlickable in your own application, for the simple cases.  It can be used as a component in its current form.  It’s also not that hard to re-create a one-off if you just need part of the functionality: if you want to use the mouse wheel to move a bigger item back and forth inside of a smaller item that defines a viewport for example, you can use WheelHandler and BoundaryRule together.  Add a DragHandler to make it possible with touch too.

Can we ever change accept() to mean something else?  It has so much history.  Should we just document that it should not normally be used with pointer events in Qt Quick, because stopping propagation is an extreme thing to do?  Should we change behavior so that it doesn’t cause a grab, and therefore you are forced to think about that as a separate thing?  Many items need to grab; most don’t need to stop propagation.

If you want an exclusive grab, you can get it explicitly now by calling QPointerEvent::setExclusiveGrabber(const QEventPoint &point, QObject *exclusiveGrabber); note that means you should do it while handling an event, not in any other context.  Yes this works the same for mouse and touch events.  Likewise even items can be passive grabbers now, in theory… I just haven’t really used it.  Maybe flickable could do that, if it doesn’t end up working to refactor it into multiple internal components.

What would the code look like for a mouse event?

void MyItem::mousePressEvent(QMouseEvent *event)
{
	if (whatever)
    		event->setExclusiveGrabber(event->points().first(), this);
}

Yeah, a bit clumsy-looking, but this is perhaps what you should do, instead of accepting the event, to only grab but still let the event keep going to other items underneath, including parents.  I wanted to add the setExclusiveGrabber() function to QEventPoint, but people didn’t like it during code review, so that’s why it’s that way.  Anyhow, you have to get the QEventPoint out of the event in order to set its grabber.  I suppose we could add a simpler setExclusiveGrabber() to QSinglePointEvent though (QMouseEvent inherits that).

To really open up the possibilities for items, I wonder if we should add a few more virtual functions to QQuickItem now.  A QQuickPointerHandler subclass can see all the touchpoints, not just the ones that are inside its parent item’s bounds.  The event gets there via QQuickItemPrivate::handlePointerEvent(), which is virtual, so you can also subclass QQuickItemPrivate if you want to make an item that handles arbitrary pointer events as-they-come, rather than those jit-constructed touch events that only have the points that are inside.  I want to make QQuickPointerHandler public when we are really sure the API is OK (soon enough in the 6.x series?) but maybe it would be useful for items too.  And now we have the last chance to add new virtual functions to public classes, for a while.  Another thing is to handle QTabletEvents: you can subclass QQuickItem, override event() and see all the events, but maybe we should add a new tabletEvent() virtual function.  I have been on the fence about that, because I want to ship a new handler that I already wrote, which should work for basic tablet-drawing applications.  (Too bad the only way to draw strokes so far is with Canvas or with Shapes.  We need to add something else for inking eventually.)  And maybe if the only convenient way is to use that handler, it would be a carrot for users to finally try pointer handlers, if they haven’t yet.  But people with long experience usually start by thinking they have to subclass QQuickItem, so it’s a surprise if it’s not easy to handle tablet events that way… now that we are finally delivering them in Qt Quick.

Another API wart is those QQuickItem::mouseUngrabEvent() and touchUngrabEvent() virtual functions.  They look like event handlers but they don’t take events as arguments.  Why is that?  Well, there is no touch ungrab event type at all, and QEvent::UngrabMouse is rarely used: it’s basically just a way of informing a Flickable that has taken a grab during filtering that it has now lost its grab.  (And yeah, again there’s that assumption that the only pointing device is one mouse.  So mouseUngrabEvent() has to be called in mouse-from-touch scenarios too, which is ugly.)  Maybe also for the case when a window loses a window-system mouse grab, but who does that?  it never seems to come up in Qt Quick.  If there were a QEvent::TouchUngrab, it would have to specify which QEventPoints are losing the grab.  But what I use now is a signal, QPointingDevice::grabChanged(QObject *grabber, GrabTransition transition, const QPointerEvent *event, const QEventPoint &point).  QQuickWindow has to connect to that signal for each device that sends events, which is more than one connection, but it also works both ways: to inform the window which item has gained the grab of either kind (exclusive or passive), and also which item or handler has lost it.  QQuickWindow then dispatches to the items and handlers to inform them via mouseUngrabEvent(), touchUngrabEvent() and QQuickPointerHandler::onGrabChanged().  Is an event so much better than a signal, that we ought to take the trouble to switch over to doing it that way?  It would take time, so it would have to be worthwhile.  But otherwise we could easily add a similar onGrabChanged() virtual function to QQuickItem.  It really needs to know more than just that it lost some mouse or touch grab: those existing virtuals don’t even identify the device.

The onSignal() pattern for virtual functions is something I like a bit, because it’s similar to how you write a signal handler in QML.  But it’s an uncommon pattern so far in C++, for some reason.  Is the existing pattern better, or should we say it’s OK to write C++ virtual signal handlers that way too?

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.qt-project.org/pipermail/development/attachments/20201008/a6e6437c/attachment-0001.html>


More information about the Development mailing list