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?
_______________________________________________
Development mailing list
[email protected]
https://lists.qt-project.org/listinfo/development