Qt
Internal/Contributor docs for the Qt SDK. <b>Note:</b> These are NOT official API docs; those are found <a href='https://doc.qt.io/'>here</a>.
Loading...
Searching...
No Matches
Dragging one DragHandler with the mouse

Most QMouseEvent instances are stack-allocated in QGuiApplicationPrivate::processMouseEvent() and sent to the application via QGuiApplication::sendSpontaneousEvent(QWindow*, QMouseEvent*). If the window is a QQuickWindow, the mouse event arrives to QQuickWindow::event() which then dispatches to the QQuickDeliveryAgent. (The reason why QQuickDeliveryAgent exists is because a tree of 2D QQuickItems may exist directly in a window, or as a subscene in some 3D object that is rendered by a View3D. Then the subscene will have its own separate QQuickDeliveryAgent instance. But another benefit is that QQuickDeliveryAgent contains most of the event delivery code, so qquickwindow.cpp is not as huge in Qt 6.1 and newer as it was before then.)

Since Qt 5.8, each QMouseEvent in Qt 5 needed to be wrapped in a QQuickPointerMouseEvent to give us a QtQuick-suitable interface for delivery. In Qt 6, QMouseEvent inherits QPointerEvent, which is intended to be suitable for generic pointer-event delivery algorithms (so QQuickPointerEvent and its subclasses have been removed). Parts of the code are generic, for delivering mouse, QTouchEvent and QTabletEvent instances in the same way. As a long-term goal, all the delivery code ideally should be generic; but so far, we still have some mouse-specific aspects.

Mouse press on a DragHandler

Let's look at what happens when a mouse button is pressed and then dragged while the mouse cursor is already hovering over a QQuickWindow containing a Rectangle with a DragHandler:

import QtQuick
Rectangle {
width: 100
height: 100
color: "lightsteelblue"
DragHandler { }
}

After the mouse button is pressed and the mouse press event is fully delivered, the ending state is that QPointingDevicePrivate::EventPointData::passiveGrabbers contains a pointer to the DragHandler instance that added itself as a passive grabber, and QPointingDevicePrivate::EventPointData::passiveGrabbersContext contains a pointer (at the same list index) to the QQuickDeliveryAgent instance that was "in charge" when the passive grab occurred. (If this DragHandler had been in a subscene, it would have been the subscene DeliveryAgent instead of the window's DeliveryAgent.) QPointingDevicePrivate::EventPointData::eventPoint is the same QEventPoint instance that was delivered as part of the QMouseEvent. QMouseEvent was stack-allocated, but the QEventPoint is persistent: the reason is to remember all the things that it stores, such as the mouse position and timestamp where and when the mouse was pressed.

Mouse move: drag the DragHandler

When the mouse begins to be dragged (is moved while the button is still pressed), QGuiApplicationPrivate::processMouseEvent() handles the next QPA mouse event (QWindowSystemInterfacePrivate::MouseEvent). It calls QPointingDevicePrivate::pointById() to retrieve the QPointingDevicePrivate::EventPointData that was stored previously when the mouse press event was delivered, and updates its stored state, including the QEventPoint instance, to be current for this incoming move event. QEventPoint::globalPressPosition is not updated though: it continues to hold the position at which the mouse press occurred; therefore, any object that ends up handing the mouse move event can check to see how far it moved since press. Likewise, QEventPoint::pressTimestamp holds the time at which it was pressed (so instantaneous velocity for this one movement is easy to calculate, if you need it). Over time during multiple moves, QEventPoint::velocity() holds an averaged mouse velocity.

Mouse press delivery can be complex if there are a lot of items and handlers in the scene; but mouse move delivery is relatively simple: we need the specific delivery agent from QPointingDevicePrivate::EventPointData::exclusiveGrabberContext to visit the exclusive grabber if there is one; and for each passive grabber in QPointingDevicePrivate::EventPointData::passiveGrabbers, the delivery agent from the parallel list QPointingDevicePrivate::EventPointData::passiveGrabbersContext needs to visit the passive grabber. The purpose of grabbing is to streamline delivery of subsequent events after the press. Each item and handler that is visited on press needed to decide at that time whether it wanted to observe the moves and the releases, because only those "interested" objects will receive the mouse moves and the release.

From the perspective of DragHandler, the QMouseEvent's QSinglePointEvent::buttons() tells it that the same button is still pressed, and it needs to decide whether to transition from a passive to an exclusive grab. It does that when the distance moved is larger than QStyleHints::startDragDistance() (the implementation of this check is in QQuickPointerHandlerPrivate::dragOverThreshold()). The exclusive grab means that the DragHandler has decided to take responsibility for all further mouse movements and the release, because it knows what it's doing now: it's dragging its QQuickPointerHandler::target item (unless the target is null and some other behavior has been set up via property bindings or whatever).

TODO add sequence diagram

Drag a DragHandler in spite of a Flickable, or vice-versa

import QtQuick
Flickable {
width: 100
height: 120
contentHeight: 200
Rectangle {
width: 100
height: 100
color: "lightsteelblue"
DragHandler { }
}
}

Flickable depends on a different event-delivery mechanism: it overrides the QQuickItem::childMouseEventFilter() virtual function, and it calls QQuickItem::setFiltersChildMouseEvents() to enable filtering. That's a sort of detour during delivery: when the window's QQuickDeliveryAgent has decided to visit the DragHandler during the mouse press delivery, it must first allow the DragHandler's grandparent, the Flickable, a chance to intercept the event, by calling QQuickFlickable::childMouseEventFilter(). If childMouseEventFilter returned true, it would mean that Flickable wanted to take over the exclusive grab; but in fact it does not do that on press: like DragHandler, it must wait until the drag threshold is exceeded.

TODO add sequence diagram

During the mouse move after the press, likewise we take the same detour: QQuickFlickable::childMouseEventFilter() must be called before QQuickDragHandler::handlePointerEvent() to give it a chance to intercept the event and take over the grab. It checks whether you are dragging in a relevant direction: if the Flickable doesn't allow horizontal dragging, then it will not try to grab. But if you are dragging vertically and if it took the grab right away, the result would be that no item or handler that is a child of QQuickFlickable::contentItem() would be able to handle a drag gesture in the same direction as QQuickFlickable::flickableDirection(): Flickable would always take over, because via the childMouseEventFilter() mechanism, it's always the first to see that the drag threshold was exceeded. But we need a mechanism so that items and handlers can "win"; so since an immediate grab would be a bad idea, Flickable rather sets its own internal flag to remember to attempt to steal the grab during delivery of the next move, only if you were actually trying to drag in that direction. Afterwards, the DeliveryAgent visits DragHandler because it has a passive grab, and DragHandler is able to take the exclusive grab.

During the next mouse move, again QQuickFlickable::childMouseEventFilter() must be called before QQuickDragHandler::handlePointerEvent(), and now if it remembers that the drag threshold was exceeded in the relevant flicking direction last time, Flickable pounces: it tries to steal the exclusive grab. Will it succeed? This is one of those rare cases when QQuickPointerHandler::grabPermissions() may matter. Its docs tell you its default: it's happy to take over grabs from items, but it's also happy to give up its grab to items. So it will let Flickable take over. The actual mechanism by which grabPermissions() are enforced is in flux though: so far in Qt 6, the enforcement is not as airtight as it was in Qt 5, and that remains to be fixed.

If we were talking about a QQuickItem, a child of Flickable's contentItem handling events on its own, rather than using a DragHandler, then it would be the QQuickItem::keepMouseGrab() property that would have the same deciding vote as grabPermissions() did in the scenario described.

For the future, if we can reimplement Flickable to use pointer handlers, maybe we could simplify the delivery logic by removing this kinky childMouseEventFilter() mechanism in Qt 7. (The FakeFlickable.qml example is a prototype.) childMouseEventFilter() is not used in very many places. But passive grabs did not yet exist when Flickable was written. Another difference is that the childMouseEventFilter() mechanism is a pre-filter: it lets the parent see pointer events before its children; whereas a passive grab is a post-filter: the passive grabber gets to see the event after the exclusive grabber has already seen it. In order for the passive grab to be requested initially, that item or handler must get a chance during delivery of a press event. But if an item takes a very eager approach to event handling, accepting the event (which implies taking the exclusive grab), it can prevent any other object in delivery order from seeing the press at all. Perhaps we can solve that in some other way some day, so that each object (including Flickable) would gain more power to take passive grabs early, and couldn't be preempted so easily.

Subscene mouse press delivery

A Qt Quick 3D scene is rendered in a View3D: it is the viewport, a 2D QQuickItem subclass. But inside the 3D scene, there may be one or more subscenes composed of 2D Qt Quick items. Each subscene needs its own QQuickDeliveryAgent instance, to deliver events to those items.

Let's take a simple example:

import QtQuick
import QtQuick3D
View3D {
width: 400; height: 400
PerspectiveCamera { }
DirectionalLight { }
Model {
id: cube
source: "#Cube"
eulerRotation: Qt.vector3d(30, 45, 0)
pickable: true
materials: DefaultMaterial {
diffuseMap: Texture {
// 2D subscene
sourceItem: Rectangle { width: 100; height: 100;
color: th.pressed ? "tomato" : "wheat"
layer.enabled: true
layer.textureSize: Qt.size(100, 100)
TapHandler { id: th }
}
}
}
}
}

TODO remove the delivery to the TapHandler on the Model: not yet supported

TODO explain the scenario

Subscene mouse drag delivery

TODO explain the scenario