Revision control
Copy as Markdown
// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant reason:default
#include <QtCore/qdebug.h>
#include <QtGui/private/qevent_p.h>
#include <QtGui/private/qeventpoint_p.h>
#include <QtGui/private/qguiapplication_p.h>
#include <QtGui/qpa/qplatformtheme.h>
#include <QtQml/private/qabstractanimationjob_p.h>
#include <QtQuick/private/qquickdeliveryagent_p_p.h>
#include <QtQuick/private/qquickhoverhandler_p.h>
#include <QtQuick/private/qquickpointerhandler_p_p.h>
#if QT_CONFIG(quick_draganddrop)
#include <QtQuick/private/qquickdrag_p.h>
#endif
#include <QtQuick/private/qquickitem_p.h>
#include <QtQuick/private/qquickprofiler_p.h>
#include <QtQuick/private/qquickrendercontrol_p.h>
#include <QtQuick/private/qquickwindow_p.h>
#include <QtCore/qpointer.h>
#include <QtCore/qvarlengtharray.h>
#include <algorithm>
#include <memory>
QT_BEGIN_NAMESPACE
Q_LOGGING_CATEGORY(lcTouch, "qt.quick.touch")
Q_STATIC_LOGGING_CATEGORY(lcTouchCmprs, "qt.quick.touch.compression")
Q_LOGGING_CATEGORY(lcTouchTarget, "qt.quick.touch.target")
Q_LOGGING_CATEGORY(lcMouse, "qt.quick.mouse")
Q_STATIC_LOGGING_CATEGORY(lcMouseTarget, "qt.quick.mouse.target")
Q_STATIC_LOGGING_CATEGORY(lcTablet, "qt.quick.tablet")
Q_LOGGING_CATEGORY(lcPtr, "qt.quick.pointer")
Q_STATIC_LOGGING_CATEGORY(lcPtrLoc, "qt.quick.pointer.localization")
Q_STATIC_LOGGING_CATEGORY(lcWheelTarget, "qt.quick.wheel.target")
Q_LOGGING_CATEGORY(lcHoverTrace, "qt.quick.hover.trace")
Q_LOGGING_CATEGORY(lcHoverCursor, "qt.quick.hover.cursor")
Q_LOGGING_CATEGORY(lcFocus, "qt.quick.focus")
Q_STATIC_LOGGING_CATEGORY(lcContextMenu, "qt.quick.contextmenu")
extern Q_GUI_EXPORT bool qt_sendShortcutOverrideEvent(QObject *o, ulong timestamp, int k, Qt::KeyboardModifiers mods, const QString &text = QString(), bool autorep = false, ushort count = 1);
bool QQuickDeliveryAgentPrivate::subsceneAgentsExist(false);
QQuickDeliveryAgent *QQuickDeliveryAgentPrivate::currentEventDeliveryAgent(nullptr);
static bool allowSyntheticRightClick()
{
static int allowRightClick = -1;
if (allowRightClick < 0) {
bool ok = false;
allowRightClick = qEnvironmentVariableIntValue("QT_QUICK_ALLOW_SYNTHETIC_RIGHT_CLICK", &ok);
if (!ok)
allowRightClick = 1; // user didn't opt out
}
return allowRightClick != 0;
}
static QQuickDeliveryAgentPrivate::HoverItems::iterator findHoverStateByItem(QQuickDeliveryAgentPrivate::HoverItems &hoverItems, QQuickItem *item)
{
return std::find_if(hoverItems.begin(), hoverItems.end(),
[item](const QQuickDeliveryAgentPrivate::HoverItemState &hoverState) {
return hoverState.item == item;
});
}
static QQuickDeliveryAgentPrivate::HoverItems::const_iterator findHoverStateByItem(const QQuickDeliveryAgentPrivate::HoverItems &hoverItems, const QQuickItem *item)
{
return std::find_if(hoverItems.cbegin(), hoverItems.cend(),
[item](const QQuickDeliveryAgentPrivate::HoverItemState &hoverState) {
return hoverState.item == item;
});
}
/*! \internal
Marks \a state as hovered, by a device that is a fingertip if \a fromTouch is \c true.
fromTouch is refreshed only if the entry is not hovered right now: an entry whose
hoverId is 0 is a leftover from a hover that has already ended (it stays in
hoverItems until deliverHoverEvent() prunes it), so hovering it again belongs to
whichever device is establishing hover now. As long as the entry is hovered it keeps
the flag it got when hover began, so that a finger tapping an item that a cursor
already hovers cannot turn the cursor's hover into touch hover - which
clearTouchHover() would then take away when the contact ends.
*/
static void markHovered(QQuickDeliveryAgentPrivate::HoverItemState &state, uint hoverId, bool fromTouch)
{
if (state.hoverId == 0)
state.fromTouch = fromTouch;
state.hoverId = hoverId;
}
void QQuickDeliveryAgentPrivate::touchToMouseEvent(QEvent::Type type, const QEventPoint &p, const QTouchEvent *touchEvent, QMutableSinglePointEvent *mouseEvent)
{
Q_ASSERT(QCoreApplication::testAttribute(Qt::AA_SynthesizeMouseForUnhandledTouchEvents));
QMutableSinglePointEvent ret(type, touchEvent->pointingDevice(), p,
(type == QEvent::MouseMove ? Qt::NoButton : Qt::LeftButton),
(type == QEvent::MouseButtonRelease ? Qt::NoButton : Qt::LeftButton),
touchEvent->modifiers(), Qt::MouseEventSynthesizedByQt);
ret.setAccepted(true); // this now causes the persistent touchpoint to be accepted too
ret.setTimestamp(touchEvent->timestamp());
*mouseEvent = ret;
// It's very important that the recipient of the event shall be able to see that
// this "mouse" event actually comes from a touch device.
Q_ASSERT(mouseEvent->device() == touchEvent->device());
if (Q_UNLIKELY(mouseEvent->device()->type() == QInputDevice::DeviceType::Mouse))
qWarning() << "Unexpected: synthesized an indistinguishable mouse event" << mouseEvent;
}
/*!
Returns \c false if the time constraint for detecting a double-click is violated.
*/
bool QQuickDeliveryAgentPrivate::isWithinDoubleClickInterval(ulong timeInterval)
{
return timeInterval < static_cast<ulong>(QGuiApplication::styleHints()->mouseDoubleClickInterval());
}
/*!
Returns \c false if the spatial constraint for detecting a touchscreen double-tap is violated.
*/
bool QQuickDeliveryAgentPrivate::isWithinDoubleTapDistance(const QPoint &distanceBetweenPresses)
{
auto square = [](qint64 v) { return v * v; };
return square(distanceBetweenPresses.x()) + square(distanceBetweenPresses.y()) <
square(QGuiApplication::styleHints()->touchDoubleTapDistance());
}
bool QQuickDeliveryAgentPrivate::checkIfDoubleTapped(ulong newPressEventTimestamp, const QPoint &newPressPos)
{
const bool doubleClicked = isDeliveringTouchAsMouse() &&
isWithinDoubleTapDistance(newPressPos - touchMousePressPos) &&
isWithinDoubleClickInterval(newPressEventTimestamp - touchMousePressTimestamp);
if (doubleClicked) {
touchMousePressTimestamp = 0;
} else {
touchMousePressTimestamp = newPressEventTimestamp;
touchMousePressPos = newPressPos;
}
return doubleClicked;
}
void QQuickDeliveryAgentPrivate::resetIfDoubleTapPrevented(const QEventPoint &pressedPoint)
{
if (touchMousePressTimestamp > 0 &&
(!isWithinDoubleTapDistance(pressedPoint.globalPosition().toPoint() - touchMousePressPos) ||
!isWithinDoubleClickInterval(pressedPoint.timestamp() - touchMousePressTimestamp))) {
touchMousePressTimestamp = 0;
touchMousePressPos = QPoint();
}
}
/*! \internal
\deprecated events are handled by methods in which the event is an argument.
Accessor for use by legacy methods such as QQuickItem::grabMouse(),
QQuickItem::ungrabMouse(), and QQuickItem::grabTouchPoints() which
are not given sufficient context to do the grabbing.
We should remove eventsInDelivery in Qt 7.
*/
QPointerEvent *QQuickDeliveryAgentPrivate::eventInDelivery() const
{
if (eventsInDelivery.isEmpty())
return nullptr;
return eventsInDelivery.top();
}
/*! \internal
A helper function for the benefit of obsolete APIs like QQuickItem::grabMouse()
that don't have the currently-being-delivered event in context.
Returns the point that acts as the mouse on the device of the currently-being-delivered
event, or \nullptr if there is no such event or the device has no such point.
*/
QPointingDevicePrivate::EventPointData *QQuickDeliveryAgentPrivate::mousePointData()
{
if (eventsInDelivery.isEmpty())
return nullptr;
auto devPriv = QPointingDevicePrivate::get(const_cast<QPointingDevice*>(eventsInDelivery.top()->pointingDevice()));
return devPriv->queryPointById(isDeliveringTouchAsMouse() ? touchMouseId : 0);
}
void QQuickDeliveryAgentPrivate::cancelTouchMouseSynthesis()
{
qCDebug(lcTouchTarget) << "id" << touchMouseId << "on" << touchMouseDevice;
touchMouseId = -1;
touchMouseDevice = nullptr;
}
bool QQuickDeliveryAgentPrivate::deliverTouchAsMouse(QQuickItem *item, QTouchEvent *pointerEvent)
{
Q_Q(QQuickDeliveryAgent);
Q_ASSERT(QCoreApplication::testAttribute(Qt::AA_SynthesizeMouseForUnhandledTouchEvents));
auto device = pointerEvent->pointingDevice();
// A touch event from a trackpad is likely to be followed by a mouse or gesture event, so mouse event synth is redundant
if (isTouchPadWithMouseEmulation(pointerEvent->device())) {
qCDebug(lcTouchTarget) << q << "skipping delivery of synth-mouse event from" << device;
return false;
}
// FIXME: make this work for mouse events too and get rid of the asTouchEvent in here.
QMutableTouchEvent event;
QQuickItemPrivate::get(item)->localizedTouchEvent(pointerEvent, false, &event);
if (!event.points().size())
return false;
// For each point, check if it is accepted, if not, try the next point.
// Any of the fingers can become the mouse one.
// This can happen because a mouse area might not accept an event at some point but another.
for (auto &p : event.points()) {
// A new touch point
if (touchMouseId == -1 && p.state() & QEventPoint::State::Pressed) {
QPointF pos = item->mapFromScene(p.scenePosition());
// probably redundant, we check bounds in the calling function (matchingNewPoints)
if (!item->contains(pos))
break;
qCDebug(lcTouchTarget) << q << device << "TP (mouse)" << Qt::hex << p.id() << "->" << item;
QMutableSinglePointEvent mousePress;
touchToMouseEvent(QEvent::MouseButtonPress, p, &event, &mousePress);
// Send a single press and see if that's accepted
QCoreApplication::sendEvent(item, &mousePress);
event.setAccepted(mousePress.isAccepted());
if (mousePress.isAccepted()) {
touchMouseDevice = device;
touchMouseId = p.id();
const auto &pt = mousePress.point(0);
if (!mousePress.exclusiveGrabber(pt))
mousePress.setExclusiveGrabber(pt, item);
if (checkIfDoubleTapped(event.timestamp(), p.globalPosition().toPoint())) {
// since we synth the mouse event from from touch, we respect the
// QPlatformTheme::TouchDoubleTapDistance instead of QPlatformTheme::MouseDoubleClickDistance
QMutableSinglePointEvent mouseDoubleClick;
touchToMouseEvent(QEvent::MouseButtonDblClick, p, &event, &mouseDoubleClick);
QCoreApplication::sendEvent(item, &mouseDoubleClick);
event.setAccepted(mouseDoubleClick.isAccepted());
if (!mouseDoubleClick.isAccepted())
cancelTouchMouseSynthesis();
}
return true;
}
// try the next point
// Touch point was there before and moved
} else if (touchMouseDevice == device && p.id() == touchMouseId) {
if (p.state() & QEventPoint::State::Updated) {
if (touchMousePressTimestamp != 0) {
if (!isWithinDoubleTapDistance(p.globalPosition().toPoint() - touchMousePressPos))
touchMousePressTimestamp = 0; // Got dragged too far, dismiss the double tap
}
if (QQuickItem *mouseGrabberItem = qmlobject_cast<QQuickItem *>(pointerEvent->exclusiveGrabber(p))) {
QMutableSinglePointEvent me;
touchToMouseEvent(QEvent::MouseMove, p, &event, &me);
QCoreApplication::sendEvent(item, &me);
event.setAccepted(me.isAccepted());
if (me.isAccepted())
qCDebug(lcTouchTarget) << q << device << "TP (mouse)" << Qt::hex << p.id() << "->" << mouseGrabberItem;
return event.isAccepted();
} else {
// no grabber, check if we care about mouse hover
// FIXME: this should only happen once, not recursively... I'll ignore it just ignore hover now.
// hover for touch???
QMutableSinglePointEvent me;
touchToMouseEvent(QEvent::MouseMove, p, &event, &me);
if (lastMousePosition.isNull())
lastMousePosition = me.scenePosition();
QPointF last = lastMousePosition;
lastMousePosition = me.scenePosition();
deliverHoverEvent(me.scenePosition(), last, me.modifiers(), me.timestamp());
break;
}
} else if (p.state() & QEventPoint::State::Released) {
// currently handled point was released
if (QQuickItem *mouseGrabberItem = qmlobject_cast<QQuickItem *>(pointerEvent->exclusiveGrabber(p))) {
QMutableSinglePointEvent me;
touchToMouseEvent(QEvent::MouseButtonRelease, p, &event, &me);
QCoreApplication::sendEvent(item, &me);
if (pointerEvent->exclusiveGrabber(p) == mouseGrabberItem) // might have ungrabbed due to event
pointerEvent->setExclusiveGrabber(p, nullptr);
cancelTouchMouseSynthesis();
return me.isAccepted();
}
}
break;
}
}
return false;
}
/*!
Ungrabs all touchpoint grabs and/or the mouse grab from the given item \a grabber.
This should not be called when processing a release event - that's redundant.
It is called in other cases, when the points may not be released, but the item
nevertheless must lose its grab due to becoming disabled, invisible, etc.
QPointerEvent::setExclusiveGrabber() calls touchUngrabEvent() when all points are released,
but if not all points are released, it cannot be sure whether to call touchUngrabEvent()
or not; so we have to do it here.
*/
void QQuickDeliveryAgentPrivate::removeGrabber(QQuickItem *grabber, bool mouse, bool touch, bool cancel)
{
Q_Q(QQuickDeliveryAgent);
if (eventsInDelivery.isEmpty()) {
// do it the expensive way
for (auto dev : knownPointingDevices) {
auto devPriv = QPointingDevicePrivate::get(const_cast<QPointingDevice *>(dev));
devPriv->removeGrabber(grabber, cancel);
}
return;
}
auto eventInDelivery = eventsInDelivery.top();
if (Q_LIKELY(mouse) && eventInDelivery) {
auto epd = mousePointData();
if (epd && epd->exclusiveGrabber == grabber && epd->exclusiveGrabberContext.data() == q) {
QQuickItem *oldGrabber = qobject_cast<QQuickItem *>(epd->exclusiveGrabber);
qCDebug(lcMouseTarget) << "removeGrabber" << oldGrabber << "-> null";
eventInDelivery->setExclusiveGrabber(epd->eventPoint, nullptr);
}
}
if (Q_LIKELY(touch)) {
bool ungrab = false;
const auto touchDevices = QPointingDevice::devices();
for (auto device : touchDevices) {
if (device->type() != QInputDevice::DeviceType::TouchScreen)
continue;
if (QPointingDevicePrivate::get(const_cast<QPointingDevice *>(static_cast<const QPointingDevice *>(device)))->
removeExclusiveGrabber(eventInDelivery, grabber))
ungrab = true;
}
if (ungrab)
grabber->touchUngrabEvent();
}
}
/*!
\internal
Clears all exclusive and passive grabs for the points in \a pointerEvent.
We never allow any kind of grab to persist after release, unless we're waiting
for a synth event from QtGui (as with most tablet events), so for points that
are fully released, the grab is cleared.
Called when QQuickWindow::event dispatches events, or when the QQuickOverlay
has filtered an event so that it bypasses normal delivery.
*/
void QQuickDeliveryAgentPrivate::clearGrabbers(QPointerEvent *pointerEvent)
{
if (pointerEvent->isEndEvent()
&& !(isTabletEvent(pointerEvent)
&& (qApp->testAttribute(Qt::AA_SynthesizeMouseForUnhandledTabletEvents)
|| QWindowSystemInterfacePrivate::TabletEvent::platformSynthesizesMouse))) {
if (pointerEvent->isSinglePointEvent()) {
if (static_cast<QSinglePointEvent *>(pointerEvent)->buttons() == Qt::NoButton) {
auto &firstPt = pointerEvent->point(0);
pointerEvent->setExclusiveGrabber(firstPt, nullptr);
pointerEvent->clearPassiveGrabbers(firstPt);
}
} else {
for (auto &point : pointerEvent->points()) {
if (point.state() == QEventPoint::State::Released) {
pointerEvent->setExclusiveGrabber(point, nullptr);
pointerEvent->clearPassiveGrabbers(point);
}
}
}
}
}
/*! \internal
Translates QEventPoint::scenePosition() in \a touchEvent to this window.
The item-local QEventPoint::position() is updated later, not here.
*/
void QQuickDeliveryAgentPrivate::translateTouchEvent(QTouchEvent *touchEvent)
{
for (qsizetype i = 0; i != touchEvent->pointCount(); ++i) {
auto &pt = touchEvent->point(i);
QMutableEventPoint::setScenePosition(pt, pt.position());
}
}
static inline bool windowHasFocus(QQuickWindow *win)
{
const QWindow *focusWindow = QGuiApplication::focusWindow();
if (!focusWindow || win == focusWindow)
return true;
return QQuickRenderControlPrivate::isFocusWindowFor(win, focusWindow);
}
static QQuickItem *findFurthestFocusScopeAncestor(QQuickItem *item)
{
QQuickItem *parentItem = item->parentItem();
if (parentItem && parentItem->flags() & QQuickItem::ItemIsFocusScope)
return findFurthestFocusScopeAncestor(parentItem);
return item;
}
#ifdef Q_OS_WEBOS
// Temporary fix for webOS until multi-seat is implemented see QTBUG-85272
static inline bool singleWindowOnScreen(QQuickWindow *win)
{
const QWindowList windowList = QGuiApplication::allWindows();
for (int i = 0; i < windowList.count(); i++) {
QWindow *ii = windowList.at(i);
if (ii == win)
continue;
if (ii->screen() == win->screen())
return false;
}
return true;
}
#endif
/*!
Set the focus inside \a scope to be \a item.
If the scope contains the active focus item, it will be changed to \a item.
Calls notifyFocusChangesRecur for all changed items.
*/
void QQuickDeliveryAgentPrivate::setFocusInScope(QQuickItem *scope, QQuickItem *item,
Qt::FocusReason reason, FocusOptions options)
{
Q_Q(QQuickDeliveryAgent);
Q_ASSERT(item);
Q_ASSERT(scope || item == rootItem);
qCDebug(lcFocus) << q << "focus" << item << "in scope" << scope;
if (scope)
qCDebug(lcFocus) << " scopeSubFocusItem:" << QQuickItemPrivate::get(scope)->subFocusItem;
QQuickItemPrivate *scopePrivate = scope ? QQuickItemPrivate::get(scope) : nullptr;
QQuickItemPrivate *itemPrivate = QQuickItemPrivate::get(item);
QQuickItem *oldActiveFocusItem = nullptr;
QQuickItem *currentActiveFocusItem = activeFocusItem;
QQuickItem *newActiveFocusItem = nullptr;
bool sendFocusIn = false;
lastFocusReason = reason;
QVarLengthArray<QQuickItem *, 20> changed;
// Does this change the active focus?
if (item == rootItem || scopePrivate->activeFocus) {
oldActiveFocusItem = activeFocusItem;
if (item->isEnabled()) {
newActiveFocusItem = item;
while (newActiveFocusItem->isFocusScope()
&& newActiveFocusItem->scopedFocusItem()
&& newActiveFocusItem->scopedFocusItem()->isEnabled()) {
newActiveFocusItem = newActiveFocusItem->scopedFocusItem();
}
} else {
newActiveFocusItem = scope;
}
if (oldActiveFocusItem) {
#if QT_CONFIG(im)
QGuiApplication::inputMethod()->commit();
#endif
activeFocusItem = nullptr;
QQuickItem *afi = oldActiveFocusItem;
while (afi && afi != scope) {
if (QQuickItemPrivate::get(afi)->activeFocus) {
QQuickItemPrivate::get(afi)->activeFocus = false;
changed << afi;
}
afi = afi->parentItem();
}
}
}
if (item != rootItem && !(options & DontChangeSubFocusItem)) {
QQuickItem *oldSubFocusItem = scopePrivate->subFocusItem;
if (oldSubFocusItem) {
QQuickItemPrivate *priv = QQuickItemPrivate::get(oldSubFocusItem);
priv->focus = false;
priv->notifyChangeListeners(QQuickItemPrivate::Focus, &QQuickItemChangeListener::itemFocusChanged, oldSubFocusItem, reason);
changed << oldSubFocusItem;
}
QQuickItemPrivate::get(item)->updateSubFocusItem(scope, true);
}
if (!(options & DontChangeFocusProperty)) {
if (item != rootItem || windowHasFocus(rootItem->window())
#ifdef Q_OS_WEBOS
// Allow focused if there is only one window in the screen where it belongs.
// Temporary fix for webOS until multi-seat is implemented see QTBUG-85272
|| singleWindowOnScreen(rootItem->window())
#endif
) {
itemPrivate->focus = true;
itemPrivate->notifyChangeListeners(QQuickItemPrivate::Focus, &QQuickItemChangeListener::itemFocusChanged, item, reason);
changed << item;
}
}
if (newActiveFocusItem && (rootItem->hasFocus() || (rootItem->window()->type() == Qt::Popup))) {
activeFocusItem = newActiveFocusItem;
QQuickItemPrivate::get(newActiveFocusItem)->activeFocus = true;
changed << newActiveFocusItem;
QQuickItem *afi = newActiveFocusItem->parentItem();
while (afi && afi != scope) {
if (afi->isFocusScope()) {
QQuickItemPrivate::get(afi)->activeFocus = true;
changed << afi;
}
afi = afi->parentItem();
}
updateFocusItemTransform();
sendFocusIn = true;
}
// Now that all the state is changed, emit signals & events
// We must do this last, as this process may result in further changes to focus.
if (oldActiveFocusItem) {
QFocusEvent event(QEvent::FocusOut, reason);
QCoreApplication::sendEvent(oldActiveFocusItem, &event);
}
// Make sure that the FocusOut didn't result in another focus change.
if (sendFocusIn && activeFocusItem == newActiveFocusItem) {
QFocusEvent event(QEvent::FocusIn, reason);
QCoreApplication::sendEvent(newActiveFocusItem, &event);
}
if (activeFocusItem != currentActiveFocusItem)
emit rootItem->window()->focusObjectChanged(activeFocusItem);
if (!changed.isEmpty())
notifyFocusChangesRecur(changed.data(), changed.size() - 1, reason);
if (isSubsceneAgent) {
auto da = QQuickWindowPrivate::get(rootItem->window())->deliveryAgent;
qCDebug(lcFocus) << " delegating setFocusInScope to" << da;
// When setting subFocusItem, hierarchy is important. Each focus ancestor's
// subFocusItem must be its nearest descendant with focus. Changing the rootItem's
// subFocusItem to 'item' here would make 'item' the subFocusItem of all ancestor
// focus scopes up until root item.
// That is why we should avoid altering subFocusItem until having traversed
// all the focus hierarchy.
QQuickItem *ancestorFS = findFurthestFocusScopeAncestor(item);
if (ancestorFS != item)
options |= QQuickDeliveryAgentPrivate::DontChangeSubFocusItem;
QQuickWindowPrivate::get(rootItem->window())->deliveryAgentPrivate()->setFocusInScope(da->rootItem(), item, reason, options);
}
if (oldActiveFocusItem == activeFocusItem)
qCDebug(lcFocus) << " activeFocusItem remains" << activeFocusItem << "in" << q;
else
qCDebug(lcFocus) << " activeFocusItem" << oldActiveFocusItem << "->" << activeFocusItem << "in" << q;
}
void QQuickDeliveryAgentPrivate::clearFocusInScope(QQuickItem *scope, QQuickItem *item, Qt::FocusReason reason, FocusOptions options)
{
Q_ASSERT(item);
Q_ASSERT(scope || item == rootItem);
Q_Q(QQuickDeliveryAgent);
qCDebug(lcFocus) << q << "clear focus" << item << "in scope" << scope;
QQuickItemPrivate *scopePrivate = nullptr;
if (scope) {
scopePrivate = QQuickItemPrivate::get(scope);
if ( !scopePrivate->subFocusItem )
return; // No focus, nothing to do.
}
QQuickItem *currentActiveFocusItem = activeFocusItem;
QQuickItem *oldActiveFocusItem = nullptr;
QQuickItem *newActiveFocusItem = nullptr;
lastFocusReason = reason;
QVarLengthArray<QQuickItem *, 20> changed;
Q_ASSERT(item == rootItem || item == scopePrivate->subFocusItem);
// Does this change the active focus?
if (item == rootItem || scopePrivate->activeFocus) {
oldActiveFocusItem = activeFocusItem;
newActiveFocusItem = scope;
#if QT_CONFIG(im)
QGuiApplication::inputMethod()->commit();
#endif
activeFocusItem = nullptr;
if (oldActiveFocusItem) {
QQuickItem *afi = oldActiveFocusItem;
while (afi && afi != scope) {
if (QQuickItemPrivate::get(afi)->activeFocus) {
QQuickItemPrivate::get(afi)->activeFocus = false;
changed << afi;
}
afi = afi->parentItem();
}
}
}
if (item != rootItem && !(options & DontChangeSubFocusItem)) {
QQuickItem *oldSubFocusItem = scopePrivate->subFocusItem;
if (oldSubFocusItem && !(options & DontChangeFocusProperty)) {
QQuickItemPrivate *priv = QQuickItemPrivate::get(oldSubFocusItem);
priv->focus = false;
priv->notifyChangeListeners(QQuickItemPrivate::Focus, &QQuickItemChangeListener::itemFocusChanged, oldSubFocusItem, reason);
changed << oldSubFocusItem;
}
QQuickItemPrivate::get(item)->updateSubFocusItem(scope, false);
} else if (!(options & DontChangeFocusProperty)) {
QQuickItemPrivate *priv = QQuickItemPrivate::get(item);
priv->focus = false;
priv->notifyChangeListeners(QQuickItemPrivate::Focus, &QQuickItemChangeListener::itemFocusChanged, item, reason);
changed << item;
}
if (newActiveFocusItem) {
Q_ASSERT(newActiveFocusItem == scope);
activeFocusItem = scope;
updateFocusItemTransform();
}
// Now that all the state is changed, emit signals & events
// We must do this last, as this process may result in further changes to focus.
if (oldActiveFocusItem) {
QFocusEvent event(QEvent::FocusOut, reason);
QCoreApplication::sendEvent(oldActiveFocusItem, &event);
}
// Make sure that the FocusOut didn't result in another focus change.
if (newActiveFocusItem && activeFocusItem == newActiveFocusItem) {
QFocusEvent event(QEvent::FocusIn, reason);
QCoreApplication::sendEvent(newActiveFocusItem, &event);
}
QQuickWindow *rootItemWindow = rootItem->window();
if (activeFocusItem != currentActiveFocusItem && rootItemWindow)
emit rootItemWindow->focusObjectChanged(activeFocusItem);
if (!changed.isEmpty())
notifyFocusChangesRecur(changed.data(), changed.size() - 1, reason);
if (isSubsceneAgent && rootItemWindow) {
auto da = QQuickWindowPrivate::get(rootItemWindow)->deliveryAgent;
qCDebug(lcFocus) << " delegating clearFocusInScope to" << da;
QQuickWindowPrivate::get(rootItemWindow)->deliveryAgentPrivate()->clearFocusInScope(da->rootItem(), item, reason, options);
}
if (oldActiveFocusItem == activeFocusItem)
qCDebug(lcFocus) << "activeFocusItem remains" << activeFocusItem << "in" << q;
else
qCDebug(lcFocus) << " activeFocusItem" << oldActiveFocusItem << "->" << activeFocusItem << "in" << q;
}
void QQuickDeliveryAgentPrivate::clearFocusObject()
{
if (activeFocusItem == rootItem)
return;
clearFocusInScope(rootItem, QQuickItemPrivate::get(rootItem)->subFocusItem, Qt::OtherFocusReason);
}
void QQuickDeliveryAgentPrivate::notifyFocusChangesRecur(QQuickItem **items, int remaining, Qt::FocusReason reason)
{
QPointer<QQuickItem> item(*items);
if (item) {
QQuickItemPrivate *itemPrivate = QQuickItemPrivate::get(item);
if (itemPrivate->notifiedFocus != itemPrivate->focus) {
itemPrivate->notifiedFocus = itemPrivate->focus;
itemPrivate->notifyChangeListeners(QQuickItemPrivate::Focus, &QQuickItemChangeListener::itemFocusChanged, item, reason);
emit item->focusChanged(itemPrivate->focus);
}
if (item && itemPrivate->notifiedActiveFocus != itemPrivate->activeFocus) {
itemPrivate->notifiedActiveFocus = itemPrivate->activeFocus;
itemPrivate->itemChange(QQuickItem::ItemActiveFocusHasChanged, bool(itemPrivate->activeFocus));
itemPrivate->notifyChangeListeners(QQuickItemPrivate::Focus, &QQuickItemChangeListener::itemFocusChanged, item, reason);
emit item->activeFocusChanged(itemPrivate->activeFocus);
}
}
if (remaining)
notifyFocusChangesRecur(items + 1, remaining - 1, reason);
}
bool QQuickDeliveryAgentPrivate::clearHover(ulong timestamp)
{
if (hoverItems.isEmpty())
return false;
QQuickWindow *window = rootItem->window();
if (!window)
return false;
const auto globalPos = QGuiApplicationPrivate::lastCursorPosition;
const QPointF lastPos = window->mapFromGlobal(globalPos);
const auto modifiers = QGuiApplication::keyboardModifiers();
// while we don't modify hoveritems directly in the loop, the delivery of the event
// is expected to reset the stored ID for each cleared item, and items might also
// be removed from the list in response to event delivery.
// So we don't want to iterate over a const version of hoverItems here (it would be
// misleading), but still use const_iterators to avoid premature detach and constant
// ref-count-checks.
for (auto it = hoverItems.cbegin(); it != hoverItems.cend(); ++it) {
if (QQuickItem *item = it->item) {
deliverHoverEventToItem(item, item->mapFromScene(lastPos), lastPos, lastPos,
globalPos, modifiers, timestamp, HoverChange::Clear);
Q_ASSERT(([this, item]{
const auto it2 = findHoverStateByItem(std::as_const(hoverItems), item);
return it2 == hoverItems.cend() || it2->hoverId == 0;
}()));
}
}
return true;
}
/*! \internal
Returns \c true if \a item is currently hovered by a device that hovers (mouse,
or stylus in proximity) rather than only because a fingertip is touching it.
An item that sets its own hover state outside deliverHoverEvent()'s bookkeeping
(see QQuickMouseArea::mousePressEvent()) needs this to tell whether lifting a
finger should end its hover, or whether a cursor is still resting on it.
*/
bool QQuickDeliveryAgentPrivate::isHoveredByHoveringDevice(const QQuickItem *item) const
{
const auto it = findHoverStateByItem(hoverItems, item);
if (it != hoverItems.cend() && it->hoverId != 0 && !it->fromTouch)
return true;
// deliverHoverEvent() erases an item's entry as soon as the finger moves off,
// so a cursor-established entry can be replaced by a touch-established hover
// while the cursor has not moved at all. Fall back to where the cursor is.
// On a platform with no cursor, lastCursorPosition is never written; this returns false.
if (!item->window())
return false;
const QPointF cursorScenePos =
item->window()->mapFromGlobal(QPointF(QGuiApplicationPrivate::lastCursorPosition));
return item->contains(item->mapFromScene(cursorScenePos));
}
/*! \internal
Clears hover on those items whose hover was established by a fingertip touching
them, and leaves alone any item that a hovering device - a mouse cursor, or a
stylus in proximity - is hovering.
Called when the last finger is lifted: a fingertip has no hover state that
outlives contact, so its hover has to end with the contact (QTBUG-62912). But a
cursor resting on an item goes on hovering it regardless of what the fingers do,
which is why this is not simply clearHover().
*/
void QQuickDeliveryAgentPrivate::clearTouchHover(ulong timestamp)
{
if (hoverItems.isEmpty())
return;
QQuickWindow *window = rootItem->window();
if (!window)
return;
const auto globalPos = QGuiApplicationPrivate::lastCursorPosition;
const QPointF lastPos = window->mapFromGlobal(globalPos);
const auto modifiers = QGuiApplication::keyboardModifiers();
// Take a copy of the items to clear before delivering anything: delivery can both
// reset the stored ids and remove entries from hoverItems (see clearHover()).
QVarLengthArray<QPointer<QQuickItem>, 4> toClear;
for (auto it = hoverItems.cbegin(); it != hoverItems.cend(); ++it) {
if (it->item && it->fromTouch && !isHoveredByHoveringDevice(it->item))
toClear.append(it->item);
}
for (const auto &item : toClear) {
if (!item)
continue;
// The position is only carried in the QHoverEvent for the benefit of anything
// that reads it in hoverLeaveEvent(); HoverChange::Clear un-hovers regardless of
// whether the position is inside the item, which matters because on a
// touch-only platform lastCursorPosition was never set by any real cursor.
deliverHoverEventToItem(item, item->mapFromScene(lastPos), lastPos, lastPos,
globalPos, modifiers, timestamp, HoverChange::Clear);
}
}
void QQuickDeliveryAgentPrivate::updateFocusItemTransform()
{
#if QT_CONFIG(im)
if (activeFocusItem && QGuiApplication::focusObject() == activeFocusItem) {
QQuickItemPrivate *focusPrivate = QQuickItemPrivate::get(activeFocusItem);
QGuiApplication::inputMethod()->setInputItemTransform(focusPrivate->itemToWindowTransform());
QGuiApplication::inputMethod()->setInputItemRectangle(QRectF(0, 0, focusPrivate->width, focusPrivate->height));
activeFocusItem->updateInputMethod(Qt::ImInputItemClipRectangle);
}
#endif
}
/*!
Returns the item that should get active focus when the
root focus scope gets active focus.
*/
QQuickItem *QQuickDeliveryAgentPrivate::focusTargetItem() const
{
if (activeFocusItem)
return activeFocusItem;
Q_ASSERT(rootItem);
QQuickItem *targetItem = rootItem;
while (targetItem->isFocusScope()
&& targetItem->scopedFocusItem()
&& targetItem->scopedFocusItem()->isEnabled()) {
targetItem = targetItem->scopedFocusItem();
}
return targetItem;
}
/*! \internal
If called during event delivery, returns the agent that is delivering the
event, without checking whether \a item is reachable from there.
Otherwise returns QQuickItemPrivate::deliveryAgent() (the delivery agent for
the narrowest subscene containing \a item), or \c null if \a item is \c null.
*/
QQuickDeliveryAgent *QQuickDeliveryAgentPrivate::currentOrItemDeliveryAgent(const QQuickItem *item)
{
if (currentEventDeliveryAgent)
return currentEventDeliveryAgent;
if (item)
return QQuickItemPrivate::get(const_cast<QQuickItem *>(item))->deliveryAgent();
return nullptr;
}
/*! \internal
QQuickDeliveryAgent delivers events to a tree of Qt Quick Items, beginning
with the given root item, which is usually QQuickWindow::rootItem() but
may alternatively be embedded into a Qt Quick 3D scene or something else.
*/
QQuickDeliveryAgent::QQuickDeliveryAgent(QQuickItem *rootItem)
: QObject(*new QQuickDeliveryAgentPrivate(rootItem), rootItem)
{
}
QQuickDeliveryAgent::~QQuickDeliveryAgent()
{
}
QQuickDeliveryAgent::Transform::~Transform()
{
}
/*! \internal
Get the QQuickRootItem or subscene root item on behalf of which
this delivery agent was constructed to handle events.
*/
QQuickItem *QQuickDeliveryAgent::rootItem() const
{
Q_D(const QQuickDeliveryAgent);
return d->rootItem;
}
/*! \internal
Returns the object that was set in setSceneTransform(): a functor that
transforms from scene coordinates in the parent scene to scene coordinates
within this DA's subscene, or \c null if none was set.
*/
QQuickDeliveryAgent::Transform *QQuickDeliveryAgent::sceneTransform() const
{
Q_D(const QQuickDeliveryAgent);
return d->sceneTransform;
}
/*! \internal
QQuickDeliveryAgent takes ownership of the given \a transform, which
encapsulates the ability to transform parent scene coordinates to rootItem
(subscene) coordinates.
*/
void QQuickDeliveryAgent::setSceneTransform(QQuickDeliveryAgent::Transform *transform)
{
Q_D(QQuickDeliveryAgent);
if (d->sceneTransform == transform)
return;
qCDebug(lcPtr) << this << d->sceneTransform << "->" << transform;
if (d->sceneTransform)
delete d->sceneTransform;
d->sceneTransform = transform;
}
/*!
Handle \a ev on behalf of this delivery agent's window or subscene.
This is the usual main entry point for every incoming event:
QQuickWindow::event() and QQuick3DViewport::forwardEventToSubscenes()
both call this function.
*/
bool QQuickDeliveryAgent::event(QEvent *ev)
{
Q_D(QQuickDeliveryAgent);
d->currentEventDeliveryAgent = this;
auto cleanup = qScopeGuard([d] { d->currentEventDeliveryAgent = nullptr; });
switch (ev->type()) {
case QEvent::MouseButtonPress:
case QEvent::MouseButtonRelease:
case QEvent::MouseButtonDblClick:
case QEvent::MouseMove: {
QMouseEvent *me = static_cast<QMouseEvent*>(ev);
d->handleMouseEvent(me);
break;
}
case QEvent::HoverEnter:
case QEvent::HoverLeave:
case QEvent::HoverMove: {
QHoverEvent *he = static_cast<QHoverEvent*>(ev);
// A QHoverEvent comes from a device that hovers, so any hover established below
// belongs to it and not to a fingertip.
d->lastMousePositionFromTouch = false;
bool accepted = d->deliverHoverEvent(he->scenePosition(),
he->points().first().sceneLastPosition(),
he->modifiers(), he->timestamp());
d->lastMousePosition = he->scenePosition();
he->setAccepted(accepted);
#if QT_CONFIG(cursor)
QQuickWindowPrivate::get(d->rootItem->window())->updateCursor(d->sceneTransform ?
d->sceneTransform->map(he->scenePosition()) : he->scenePosition(), d->rootItem);
#endif
return accepted;
}
case QEvent::TouchBegin:
case QEvent::TouchUpdate:
case QEvent::TouchEnd: {
QTouchEvent *touch = static_cast<QTouchEvent*>(ev);
d->handleTouchEvent(touch);
if (Q_LIKELY(QCoreApplication::testAttribute(Qt::AA_SynthesizeMouseForUnhandledTouchEvents))) {
// we consume all touch events ourselves to avoid duplicate
// mouse delivery by QtGui mouse synthesis
ev->accept();
}
break;
}
case QEvent::TouchCancel:
// return in order to avoid the QWindow::event below
return d->deliverTouchCancelEvent(static_cast<QTouchEvent*>(ev));
break;
case QEvent::Enter: {
if (!d->rootItem)
return false;
QEnterEvent *enter = static_cast<QEnterEvent*>(ev);
const auto scenePos = enter->scenePosition();
d->lastMousePositionFromTouch = false;
bool accepted = d->deliverHoverEvent(scenePos,
enter->points().first().sceneLastPosition(),
enter->modifiers(), enter->timestamp());
d->lastMousePosition = scenePos;
// deliverHoverEvent() constructs QHoverEvents: check that EPD didn't end up with corrupted scenePos
Q_ASSERT(enter->scenePosition() == scenePos);
enter->setAccepted(accepted);
#if QT_CONFIG(cursor)
QQuickWindowPrivate::get(d->rootItem->window())->updateCursor(enter->scenePosition(), d->rootItem);
#endif
return accepted;
}
case QEvent::Leave:
d->clearHover();
d->lastMousePosition = QPointF();
d->lastMousePositionFromTouch = false;
break;
#if QT_CONFIG(quick_draganddrop)
case QEvent::DragEnter:
case QEvent::DragLeave:
case QEvent::DragMove:
case QEvent::Drop:
d->deliverDragEvent(d->dragGrabber, ev);
break;
#endif
case QEvent::FocusAboutToChange:
#if QT_CONFIG(im)
if (d->activeFocusItem)
qGuiApp->inputMethod()->commit();
#endif
break;
#if QT_CONFIG(gestures)
case QEvent::NativeGesture:
d->deliverSinglePointEventUntilAccepted(static_cast<QPointerEvent *>(ev));
break;
#endif
case QEvent::ShortcutOverride:
d->deliverKeyEvent(static_cast<QKeyEvent *>(ev));
break;
case QEvent::InputMethod:
case QEvent::InputMethodQuery:
{
QQuickItem *target = d->focusTargetItem();
if (target)
QCoreApplication::sendEvent(target, ev);
}
break;
#if QT_CONFIG(wheelevent)
case QEvent::Wheel: {
auto event = static_cast<QWheelEvent *>(ev);
qCDebug(lcMouse) << event;
//if the actual wheel event was accepted, accept the compatibility wheel event and return early
if (d->lastWheelEventAccepted && event->angleDelta().isNull() && event->phase() == Qt::ScrollUpdate)
return true;
event->ignore();
Q_QUICK_INPUT_PROFILE(QQuickProfiler::Mouse, QQuickProfiler::InputMouseWheel,
event->angleDelta().x(), event->angleDelta().y());
d->deliverSinglePointEventUntilAccepted(event);
d->lastWheelEventAccepted = event->isAccepted();
break;
}
#endif
#if QT_CONFIG(tabletevent)
case QEvent::TabletPress:
case QEvent::TabletMove:
case QEvent::TabletRelease:
{
auto *tabletEvent = static_cast<QTabletEvent *>(ev);
d->deliverPointerEvent(tabletEvent); // visits HoverHandlers too (unlike the mouse event case)
#if QT_CONFIG(cursor)
QQuickWindowPrivate::get(d->rootItem->window())->updateCursor(tabletEvent->scenePosition(), d->rootItem);
#endif
}
break;
#endif
#ifndef QT_NO_CONTEXTMENU
case QEvent::ContextMenu:
d->deliverContextMenuEvent(static_cast<QContextMenuEvent *>(ev));
break;
#endif
case QEvent::Timer:
Q_ASSERT(static_cast<QTimerEvent *>(ev)->timerId() == d->frameSynchronousDelayTimer.timerId());
d->frameSynchronousDelayTimer.stop();
d->flushFrameSynchronousEvents(d->rootItem->window());
break;
default:
return false;
}
return true;
}
void QQuickDeliveryAgentPrivate::deliverKeyEvent(QKeyEvent *e)
{
if (activeFocusItem) {
const bool keyPress = (e->type() == QEvent::KeyPress);
switch (e->type()) {
case QEvent::KeyPress:
Q_QUICK_INPUT_PROFILE(QQuickProfiler::Key, QQuickProfiler::InputKeyPress, e->key(), e->modifiers());
break;
case QEvent::KeyRelease:
Q_QUICK_INPUT_PROFILE(QQuickProfiler::Key, QQuickProfiler::InputKeyRelease, e->key(), e->modifiers());
break;
default:
break;
}
QQuickItem *item = activeFocusItem;
// In case of generated event, trigger ShortcutOverride event
if (keyPress && e->spontaneous() == false)
qt_sendShortcutOverrideEvent(item, e->timestamp(),
e->key(), e->modifiers(), e->text(),
e->isAutoRepeat(), e->count());
do {
Q_ASSERT(e->type() != QEvent::ShortcutOverride || !e->isAccepted());
if (e->type() != QEvent::ShortcutOverride)
e->accept();
QCoreApplication::sendEvent(item, e);
} while (!e->isAccepted() && (item = item->parentItem()));
}
}
QQuickDeliveryAgentPrivate::QQuickDeliveryAgentPrivate(QQuickItem *root) :
QObjectPrivate(),
rootItem(root),
// a plain QQuickItem can be a subscene root; a QQuickRootItem always belongs directly to a QQuickWindow
isSubsceneAgent(!qmlobject_cast<QQuickRootItem *>(rootItem))
{
#if QT_CONFIG(quick_draganddrop)
dragGrabber = new QQuickDragGrabber;
#endif
if (isSubsceneAgent)
subsceneAgentsExist = true;
const auto interval = qEnvironmentVariableIntegerValue("QT_QUICK_FRAME_SYNCHRONOUS_HOVER_INTERVAL");
if (interval.has_value()) {
qCDebug(lcHoverTrace) << "frame-synchronous hover interval" << interval;
frameSynchronousHoverInterval = int(interval.value());
}
if (frameSynchronousHoverInterval > 0)
frameSynchronousHoverTimer.start();
}
QQuickDeliveryAgentPrivate::~QQuickDeliveryAgentPrivate()
{
#if QT_CONFIG(quick_draganddrop)
delete dragGrabber;
dragGrabber = nullptr;
#endif
delete sceneTransform;
}
/*! \internal
Make a copy of any type of QPointerEvent, and optionally localize it
by setting its first point's local position() if \a transformedLocalPos is given.
\note some subclasses of QSinglePointEvent, such as QWheelEvent, add extra storage.
This function doesn't yet support cloning all of those; it can be extended if needed.
*/
QPointerEvent *QQuickDeliveryAgentPrivate::clonePointerEvent(QPointerEvent *event, std::optional<QPointF> transformedLocalPos)
{
QPointerEvent *ret = event->clone();
QEventPoint &point = ret->point(0);
QMutableEventPoint::detach(point);
QMutableEventPoint::setTimestamp(point, event->timestamp());
if (transformedLocalPos)
QMutableEventPoint::setPosition(point, *transformedLocalPos);
return ret;
}
void QQuickDeliveryAgentPrivate::deliverToPassiveGrabbers(const QList<QPointer <QObject> > &passiveGrabbers,
QPointerEvent *pointerEvent)
{
const QList<QObject *> &eventDeliveryTargets =
QQuickPointerHandlerPrivate::deviceDeliveryTargets(pointerEvent->device());
QVarLengthArray<std::pair<QQuickItem *, bool>, 4> sendFilteredPointerEventResult;
hasFiltered.clear();
for (QObject *grabberObject : passiveGrabbers) {
// a null pointer in passiveGrabbers is unlikely, unless the grabbing handler was deleted dynamically
if (Q_UNLIKELY(!grabberObject))
continue;
// a passiveGrabber might be an item or a handler
if (QQuickPointerHandler *handler = qobject_cast<QQuickPointerHandler *>(grabberObject)) {
if (handler && !eventDeliveryTargets.contains(handler)) {
bool alreadyFiltered = false;
QQuickItem *par = handler->parentItem();
// see if we already have sent a filter event to the parent
auto it = std::find_if(sendFilteredPointerEventResult.begin(), sendFilteredPointerEventResult.end(),
[par](const std::pair<QQuickItem *, bool> &pair) { return pair.first == par; });
if (it != sendFilteredPointerEventResult.end()) {
// Yes, the event was sent to that parent for filtering: do not call it again, but use
// the result of the previous call to determine whether we should call the handler.
alreadyFiltered = it->second;
} else if (par) {
alreadyFiltered = sendFilteredPointerEvent(pointerEvent, par);
sendFilteredPointerEventResult << std::make_pair(par, alreadyFiltered);
}
if (!alreadyFiltered) {
if (par)
localizePointerEvent(pointerEvent, par);
handler->handlePointerEvent(pointerEvent);
}
}
} else if (QQuickItem *grabberItem = static_cast<QQuickItem *>(grabberObject)) {
// don't steal the grab if input should remain with the exclusive grabber only
if (QQuickItem *excGrabber = static_cast<QQuickItem *>(pointerEvent->exclusiveGrabber(pointerEvent->point(0)))) {
if ((isMouseEvent(pointerEvent) && excGrabber->keepMouseGrab())
|| (isTouchEvent(pointerEvent) && excGrabber->keepTouchGrab())) {
return;
}
}
localizePointerEvent(pointerEvent, grabberItem);
QCoreApplication::sendEvent(grabberItem, pointerEvent);
pointerEvent->accept();
}
}
}
bool QQuickDeliveryAgentPrivate::sendHoverEvent(QEvent::Type type, QQuickItem *item,
const QPointF &localPos, const QPointF &scenePos, const QPointF &lastScenePos,
const QPointF &globalPos, Qt::KeyboardModifiers modifiers, ulong timestamp)
{
QHoverEvent hoverEvent(type, scenePos, globalPos, lastScenePos, modifiers);
hoverEvent.setTimestamp(timestamp);
hoverEvent.setAccepted(true);
QEventPoint &point = hoverEvent.point(0);
QMutableEventPoint::setPosition(point, localPos);
if (Q_LIKELY(item->window()))
QMutableEventPoint::setGlobalLastPosition(point, item->window()->mapToGlobal(lastScenePos));
hasFiltered.clear();
if (sendFilteredMouseEvent(&hoverEvent, item, item->parentItem()))
return true;
QCoreApplication::sendEvent(item, &hoverEvent);
return hoverEvent.isAccepted();
}
/*! \internal
Delivers a hover event at \a scenePos to the whole scene or subscene
that this DeliveryAgent is responsible for. Returns \c true if
delivery is "done".
*/
// TODO later: specify the device in case of multi-mouse scenario, or mouse and tablet both in use
bool QQuickDeliveryAgentPrivate::deliverHoverEvent(
const QPointF &scenePos, const QPointF &lastScenePos,
Qt::KeyboardModifiers modifiers, ulong timestamp)
{
// The first time this function is called, hoverItems is empty.
// We then call deliverHoverEventRecursive from the rootItem, and
// populate the list with all the children and grandchildren that
// we find that should receive hover events (in addition to sending
// hover events to them and their HoverHandlers). We also set the
// hoverId for each item to the currentHoverId.
// The next time this function is called, we bump currentHoverId,
// and call deliverHoverEventRecursive once more.
// When that call returns, the list will contain the items that
// were hovered the first time, as well as the items that were hovered
// this time. But only the items that were hovered this time
// will have their hoverId equal to currentHoverId; the ones we didn't
// visit will still have an old hoverId. We can therefore go through the
// list at the end of this function and look for items with an old hoverId,
// remove them from the list, and update their state accordingly.
const bool subtreeHoverEnabled = QQuickItemPrivate::get(rootItem)->subtreeHoverEnabled;
const bool itemsWasHovered = !hoverItems.isEmpty();
if (!subtreeHoverEnabled && !itemsWasHovered)
return false;
currentHoverId++;
if (subtreeHoverEnabled) {
hoveredLeafItemFound = false;
QQuickPointerHandlerPrivate::deviceDeliveryTargets(QPointingDevice::primaryPointingDevice()).clear();
deliverHoverEventRecursive(rootItem, scenePos, scenePos, lastScenePos,
rootItem->mapToGlobal(scenePos), modifiers, timestamp);
}
// Prune the list for items that are no longer hovered
for (auto it = hoverItems.begin(); it != hoverItems.end();) {
const auto &[item, hoverId, fromFinger] = *it;
if (hoverId == currentHoverId) {
// Still being hovered
it++;
} else {
// No longer hovered. If hoverId is 0, it means that we have sent a HoverLeave
// event to the item already, and it can just be removed from the list. Note that
// the item can have been deleted as well.
if (item && hoverId != 0)
deliverHoverEventToItem(item, item->mapFromScene(scenePos), scenePos, lastScenePos,
QGuiApplicationPrivate::lastCursorPosition, modifiers, timestamp, HoverChange::Clear);
it = hoverItems.erase(it);
}
}
const bool itemsAreHovered = !hoverItems.isEmpty();
return itemsWasHovered || itemsAreHovered;
}
/*! \internal
Delivers a hover event at \a scenePos to \a item and all its children.
The children get it first. As soon as any item allows the event to remain
accepted, recursion stops. Returns \c true in that case, or \c false if the
event is rejected.
Each item that has hover enabled (from setAcceptHoverEvents()) has the
QQuickItemPrivate::hoverEnabled flag set. This only controls whether we
should send hover events to the item itself. (HoverHandlers no longer set
this flag.) When an item has hoverEnabled set, all its ancestors have the
QQuickItemPrivate::subtreeHoverEnabled set. This function will
follow the subtrees that have subtreeHoverEnabled by recursing into each
child with that flag set. And for each child (in addition to the item
itself) that also has hoverEnabled set, we call deliverHoverEventToItem()
to actually deliver the event to it. The item can then choose to accept or
reject the event. This is only for control over whether we stop propagation
or not: an item can reject the event, but at the same time be hovered (and
therefore in hoverItems). By accepting the event, the item will effectivly
end up as the only one hovered. Any other HoverHandler that may be a child
of an item that is stacked underneath, will not. Note that since siblings
can overlap, there can be more than one leaf item under the mouse.
Note that HoverHandler doesn't set the hoverEnabled flag on the parent item.
But still, adding a HoverHandler to an item will set its subtreeHoverEnabled flag.
So all the propagation logic described above will otherwise be the same.
But the hoverEnabled flag can be used to resolve if subtreeHoverEnabled is on
because the application explicitly requested it (setAcceptHoverEvents()), or
indirectly, because the item has HoverHandlers.
For legacy reasons (Qt 6.1), as soon as we find a leaf item that has hover
enabled, and therefore receives the event, we stop recursing into the remaining
siblings (even if the event was ignored). This means that we only allow hover
events to propagate up the direct parent-child hierarchy, and not to siblings.
However, if the first candidate HoverHandler is disabled, delivery continues
to the next one, which may be a sibling (QTBUG-106548).
*/
bool QQuickDeliveryAgentPrivate::deliverHoverEventRecursive(QQuickItem *item,
const QPointF &localPos, const QPointF &scenePos, const QPointF &lastScenePos, const QPointF &globalPos,
Qt::KeyboardModifiers modifiers, ulong timestamp)
{
const QQuickItemPrivate *itemPrivate = QQuickItemPrivate::get(item);
const QList<QQuickItem *> children = itemPrivate->paintOrderChildItems();
const bool hadChildrenChanged = itemPrivate->dirtyAttributes & QQuickItemPrivate::ChildrenChanged;
for (int ii = children.size() - 1; ii >= 0; --ii) {
// If the children had not changed before we started the loop, but now they have changed,
// stop looping to avoid potentially dereferencing a dangling pointer.
// This is unusual, and hover delivery occurs frequently anyway, so just wait until next time.
if (!hadChildrenChanged && Q_UNLIKELY(itemPrivate->dirtyAttributes & QQuickItemPrivate::ChildrenChanged))
break;
QQuickItem *child = children.at(ii);
const QQuickItemPrivate *childPrivate = QQuickItemPrivate::get(child);
if (!child->isVisible() || childPrivate->culled)
continue;
if (!childPrivate->subtreeHoverEnabled)
continue;
QTransform childToParent;
childPrivate->itemToParentTransform(&childToParent);
const QPointF childLocalPos = childToParent.inverted().map(localPos);
// If the child clips, or all children are inside, and scenePos is
// outside its rectangular bounds, we can skip this item and all its
// children, to save time.
if (childPrivate->effectivelyClipsEventHandlingChildren() &&
!childPrivate->eventHandlingBounds().contains(childLocalPos)) {
#ifdef QT_BUILD_INTERNAL
++QQuickItemPrivate::effectiveClippingSkips_counter;
#endif
continue;
}
// Recurse into the child
const bool accepted = deliverHoverEventRecursive(child, childLocalPos, scenePos, lastScenePos, globalPos, modifiers, timestamp);
if (accepted) {
// Stop propagation / recursion
return true;
}
if (hoveredLeafItemFound) {
// Don't propagate to siblings, only to ancestors
break;
}
}
// All decendants have been visited.
// Now deliver the event to the item
return deliverHoverEventToItem(item, localPos, scenePos, lastScenePos, globalPos, modifiers, timestamp, HoverChange::Set);
}
/*! \internal
Delivers a hover event at \a scenePos to \a item and its HoverHandlers if any.
Returns \c true if the event remains accepted, \c false if rejected.
If \a clearHover is \c true, it will be sent as a QEvent::HoverLeave event,
and the item and its handlers are expected to transition into their non-hovered
states even if the position still indicates that the mouse is inside.
*/
bool QQuickDeliveryAgentPrivate::deliverHoverEventToItem(
QQuickItem *item, const QPointF &localPos, const QPointF &scenePos, const QPointF &lastScenePos,
const QPointF &globalPos, Qt::KeyboardModifiers modifiers, ulong timestamp, HoverChange hoverChange)
{
QQuickItemPrivate *itemPrivate = QQuickItemPrivate::get(item);
const bool isHovering = item->contains(localPos);
auto hoverItemIterator = findHoverStateByItem(hoverItems, item);
const bool wasHovering = hoverItemIterator != hoverItems.end() && hoverItemIterator->hoverId != 0;
qCDebug(lcHoverTrace) << "item:" << item << "scene pos:" << scenePos << "localPos:" << localPos
<< "wasHovering:" << wasHovering << "isHovering:" << isHovering;
bool accepted = false;
// Start by sending out enter/move/leave events to the item.
// Note that hoverEnabled only controls if we should send out hover events to the
// item itself. HoverHandlers are not included, and are dealt with separately below.
if (itemPrivate->hoverEnabled && isHovering && hoverChange == HoverChange::Set) {
// Add the item to the list of hovered items (if it doesn't exist there
// from before), and update hoverId to mark that it's (still) hovered.
// Also set hoveredLeafItemFound, so that only propagate in a straight
// line towards the root from now on.
hoveredLeafItemFound = true;
if (hoverItemIterator != hoverItems.end())
markHovered(*hoverItemIterator, currentHoverId, lastMousePositionFromTouch);
else
hoverItems.append({item, currentHoverId, lastMousePositionFromTouch});
if (wasHovering)
accepted = sendHoverEvent(QEvent::HoverMove, item, localPos, scenePos, lastScenePos, globalPos, modifiers, timestamp);
else
accepted = sendHoverEvent(QEvent::HoverEnter, item, localPos, scenePos, lastScenePos, globalPos, modifiers, timestamp);
} else if (wasHovering) {
// A leave should never stop propagation
hoverItemIterator->hoverId = 0;
sendHoverEvent(QEvent::HoverLeave, item, localPos, scenePos, lastScenePos, globalPos, modifiers, timestamp);
}
if (!itemPrivate->hasPointerHandlers())
return accepted;
// Next, send out hover events to the hover handlers.
// If the item didn't accept the hover event, 'accepted' is now false.
// Otherwise it's true, and then it should stay the way regardless of
// whether or not the hoverhandlers themselves are hovered.
// Note that since a HoverHandler can have a margin, a HoverHandler
// can be hovered even if the item itself is not.
if (hoverChange == HoverChange::Clear) {
// Note: a leave should never stop propagation
QHoverEvent hoverEvent(QEvent::HoverLeave, scenePos, globalPos, lastScenePos, modifiers);
hoverEvent.setTimestamp(timestamp);
for (QQuickPointerHandler *h : itemPrivate->extra->pointerHandlers) {
if (QQuickHoverHandler *hh = qmlobject_cast<QQuickHoverHandler *>(h)) {
if (!hh->isHovered())
continue;
hoverEvent.setAccepted(true);
QCoreApplication::sendEvent(hh, &hoverEvent);
}
}
} else {
QMouseEvent hoverEvent(QEvent::MouseMove, localPos, scenePos, globalPos, Qt::NoButton, Qt::NoButton, modifiers);
hoverEvent.setTimestamp(timestamp);
for (QQuickPointerHandler *h : itemPrivate->extra->pointerHandlers) {
if (QQuickHoverHandler *hh = qmlobject_cast<QQuickHoverHandler *>(h)) {
if (!hh->enabled())
continue;
hoverEvent.setAccepted(true);
hh->handlePointerEvent(&hoverEvent);
if (hh->isHovered()) {
// Mark the whole item as updated, even if only the handler is
// actually in a hovered state (because of HoverHandler.margins)
hoveredLeafItemFound = true;
hoverItemIterator = findHoverStateByItem(hoverItems, item);
if (hoverItemIterator != hoverItems.end())
markHovered(*hoverItemIterator, currentHoverId, lastMousePositionFromTouch);
else
hoverItems.append({item, currentHoverId, lastMousePositionFromTouch});
if (hh->isBlocking()) {
qCDebug(lcHoverTrace) << "skipping rest of hover delivery due to blocking" << hh;
accepted = true;
break;
}
}
}
}
}
return accepted;
}
// Simple delivery of non-mouse, non-touch Pointer Events: visit the items and handlers
// in the usual reverse-paint-order until propagation is stopped
bool QQuickDeliveryAgentPrivate::deliverSinglePointEventUntilAccepted(QPointerEvent *event)
{
Q_ASSERT(event->points().size() == 1);
QQuickPointerHandlerPrivate::deviceDeliveryTargets(event->pointingDevice()).clear();
QEventPoint &point = event->point(0);
QList<QQuickItem *> targetItems = pointerTargets(rootItem, event, point, false, false);
point.setAccepted(false);
// Let passive grabbers see the event. This must be done before we deliver the
// event to the target and to handlers that might stop event propagation.
// Passive grabbers cannot stop event delivery.
for (const auto &passiveGrabber : event->passiveGrabbers(point)) {
if (auto *grabberItem = qobject_cast<QQuickItem *>(passiveGrabber)) {
if (targetItems.contains(grabberItem))
continue;
localizePointerEvent(event, grabberItem);
QCoreApplication::sendEvent(grabberItem, event);
}
}
// Maintain the invariant that items receive input events in accepted state.
// A passive grabber might have explicitly ignored the event.
event->accept();
for (QQuickItem *item : targetItems) {
QQuickItemPrivate *itemPrivate = QQuickItemPrivate::get(item);
localizePointerEvent(event, item);
// Let Pointer Handlers have the first shot
itemPrivate->handlePointerEvent(event);
if (point.isAccepted())
return true;
event->accept();
QCoreApplication::sendEvent(item, event);
if (event->isAccepted()) {
qCDebug(lcWheelTarget) << event << "->" << item;
return true;
}
}
return false; // it wasn't handled
}
bool QQuickDeliveryAgentPrivate::deliverTouchCancelEvent(QTouchEvent *event)
{
qCDebug(lcTouch) << event;
// An incoming TouchCancel event will typically not contain any points,
// but sendTouchCancelEvent() adds the points that have grabbers to the event.
// Deliver it to all items and handlers that have active touches.
const_cast<QPointingDevicePrivate *>(QPointingDevicePrivate::get(event->pointingDevice()))->
sendTouchCancelEvent(event);
cancelTouchMouseSynthesis();
// A cancelled sequence ends just as definitely as a released one,
// so hover that the fingers established has to go too.
if (event->pointerType() == QPointingDevice::PointerType::Finger)
clearTouchHover(event->timestamp());
// Whatever the pointer type, a cancelled sequence must not leave frame-synchronous
// hover delivery running at the position where contact was lost.
lastMousePosition = {};
lastMousePositionFromTouch = false;
return true;
}
void QQuickDeliveryAgentPrivate::deliverDelayedTouchEvent()
{
// Deliver and delete delayedTouch.
// Set delayedTouch to nullptr before delivery to avoid redelivery in case of
// event loop recursions (e.g if it the touch starts a dnd session).
std::unique_ptr<QTouchEvent> e(std::move(delayedTouch));
qCDebug(lcTouchCmprs) << "delivering" << e.get();
compressedTouchCount = 0;
deliverPointerEvent(e.get());
}
/*! \internal
The handler for the QEvent::WindowDeactivate event, and also when
Qt::ApplicationState tells us the application is no longer active.
It clears all exclusive grabs of items and handlers whose window is this one,
for all known pointing devices.
The QEvent is not passed into this function because in the first case it's
just a plain QEvent with no extra data, and because the application state
change is delivered via a signal rather than an event.
*/
void QQuickDeliveryAgentPrivate::handleWindowDeactivate(QQuickWindow *win)
{
Q_Q(QQuickDeliveryAgent);
qCDebug(lcFocus) << "deactivated" << win->title();
const auto inputDevices = QInputDevice::devices();
for (auto device : inputDevices) {
if (auto pointingDevice = qobject_cast<const QPointingDevice *>(device)) {
auto devPriv = QPointingDevicePrivate::get(const_cast<QPointingDevice *>(pointingDevice));
for (auto epd : devPriv->activePoints.values()) {
if (!epd.exclusiveGrabber.isNull()) {
bool relevant = false;
if (QQuickItem *item = qmlobject_cast<QQuickItem *>(epd.exclusiveGrabber.data()))
relevant = (item->window() == win);
else if (QQuickPointerHandler *handler = qmlobject_cast<QQuickPointerHandler *>(epd.exclusiveGrabber.data())) {
if (handler->parentItem())
relevant = (handler->parentItem()->window() == win && epd.exclusiveGrabberContext.data() == q);
else
// a handler with no Item parent probably has a 3D Model parent.
// TODO actually check the window somehow
relevant = true;
}
if (relevant)
devPriv->setExclusiveGrabber(nullptr, epd.eventPoint, nullptr);
}
// For now, we don't clearPassiveGrabbers(), just in case passive grabs
// can be useful to keep monitoring the mouse even after window deactivation.
}
}
}
}
void QQuickDeliveryAgentPrivate::handleWindowHidden(QQuickWindow *win)
{
qCDebug(lcFocus) << "hidden" << win->title();
clearHover();
lastMousePosition = QPointF();
lastMousePositionFromTouch = false;
}
bool QQuickDeliveryAgentPrivate::allUpdatedPointsAccepted(const QPointerEvent *ev)
{
for (auto &point : ev->points()) {
if (point.state() != QEventPoint::State::Pressed && !point.isAccepted())
return false;
}
return true;
}
/*! \internal
Localize \a ev for delivery to \a dest.
Unlike QMutableTouchEvent::localized(), this modifies the QEventPoint
instances in \a ev, which is more efficient than making a copy.
*/
void QQuickDeliveryAgentPrivate::localizePointerEvent(QPointerEvent *ev, const QQuickItem *dest)
{
for (int i = 0; i < ev->pointCount(); ++i) {
auto &point = ev->point(i);
QMutableEventPoint::setPosition(point, dest->mapFromScene(point.scenePosition()));
qCDebug(lcPtrLoc) << ev->type() << "@" << point.scenePosition() << "to"
<< dest << "@" << dest->mapToScene(QPointF()) << "->" << point;
}
}
QList<QObject *> QQuickDeliveryAgentPrivate::exclusiveGrabbers(QPointerEvent *ev)
{
QList<QObject *> result;
for (const QEventPoint &point : ev->points()) {
if (QObject *grabber = ev->exclusiveGrabber(point)) {
if (!result.contains(grabber))
result << grabber;
}
}
return result;
}
bool QQuickDeliveryAgentPrivate::anyPointGrabbed(const QPointerEvent *ev)
{
for (const QEventPoint &point : ev->points()) {
if (ev->exclusiveGrabber(point) || !ev->passiveGrabbers(point).isEmpty())
return true;
}
return false;
}
bool QQuickDeliveryAgentPrivate::allPointsGrabbed(const QPointerEvent *ev)
{
for (const auto &point : ev->points()) {
if (!ev->exclusiveGrabber(point) && ev->passiveGrabbers(point).isEmpty())
return false;
}
return true;
}
bool QQuickDeliveryAgentPrivate::isMouseEvent(const QPointerEvent *ev)
{
switch (ev->type()) {
case QEvent::MouseButtonPress:
case QEvent::MouseButtonRelease:
case QEvent::MouseButtonDblClick:
case QEvent::MouseMove:
return true;
default:
return false;
}
}
bool QQuickDeliveryAgentPrivate::isMouseOrWheelEvent(const QPointerEvent *ev)
{
return isMouseEvent(ev) || ev->type() == QEvent::Wheel;
}
bool QQuickDeliveryAgentPrivate::isHoverEvent(const QPointerEvent *ev)
{
switch (ev->type()) {
case QEvent::HoverEnter:
case QEvent::HoverMove:
case QEvent::HoverLeave:
return true;
default:
return false;
}
}
bool QQuickDeliveryAgentPrivate::isTouchEvent(const QPointerEvent *ev)
{
switch (ev->type()) {
case QEvent::TouchBegin:
case QEvent::TouchUpdate:
case QEvent::TouchEnd:
case QEvent::TouchCancel:
return true;
default:
return false;
}
}
bool QQuickDeliveryAgentPrivate::isTabletEvent(const QPointerEvent *ev)
{
#if QT_CONFIG(tabletevent)
switch (ev->type()) {
case QEvent::TabletPress:
case QEvent::TabletMove:
case QEvent::TabletRelease:
case QEvent::TabletEnterProximity:
case QEvent::TabletLeaveProximity:
return true;
default:
break;
}
#else
Q_UNUSED(ev);
#endif // tabletevent
return false;
}
bool QQuickDeliveryAgentPrivate::isEventFromMouseOrTouchpad(const QPointerEvent *ev)
{
const auto devType = ev->device()->type();
return devType == QInputDevice::DeviceType::Mouse ||
devType == QInputDevice::DeviceType::TouchPad;
}
bool QQuickDeliveryAgentPrivate::isSynthMouse(const QPointerEvent *ev)
{
return (!isEventFromMouseOrTouchpad(ev) && isMouseEvent(ev));
}
/*!
Returns \c true if \a dev is a type of device that only sends
QSinglePointEvents.
*/
bool QQuickDeliveryAgentPrivate::isSinglePointDevice(const QInputDevice *dev)
{
switch (dev->type()) {
case QInputDevice::DeviceType::Mouse:
case QInputDevice::DeviceType::TouchPad:
case QInputDevice::DeviceType::Puck:
case QInputDevice::DeviceType::Stylus:
case QInputDevice::DeviceType::Airbrush:
return true;
case QInputDevice::DeviceType::TouchScreen:
case QInputDevice::DeviceType::Keyboard:
case QInputDevice::DeviceType::Unknown:
case QInputDevice::DeviceType::AllDevices:
return false;
}
return false;
}
/*!
Returns \c true if \a dev is a touch/track pad that will emulate mouse events.
*/
bool QQuickDeliveryAgentPrivate::isTouchPadWithMouseEmulation(const QInputDevice *device)
{
return device->type() == QInputDevice::DeviceType::TouchPad
&& device->capabilities().testFlag(QInputDevice::Capability::MouseEmulation);
}
QQuickPointingDeviceExtra *QQuickDeliveryAgentPrivate::deviceExtra(const QInputDevice *device)
{
QInputDevicePrivate *devPriv = QInputDevicePrivate::get(const_cast<QInputDevice *>(device));
if (devPriv->qqExtra)
return static_cast<QQuickPointingDeviceExtra *>(devPriv->qqExtra);
auto extra = new QQuickPointingDeviceExtra;
devPriv->qqExtra = extra;
QObject::connect(device, &QObject::destroyed, device, [devPriv]() {
delete static_cast<QQuickPointingDeviceExtra *>(devPriv->qqExtra);
devPriv->qqExtra = nullptr;
});
return extra;
}
/*!
\internal
This function is called from handleTouchEvent() in case a series of touch
events containing only \c Updated and \c Stationary points arrives within a
short period of time. (Some touchscreens are more "jittery" than others.)
It would be a waste of CPU time to deliver events and have items in the
scene getting modified more often than once per frame; so here we try to
coalesce the series of updates into a single event containing all updates
that occur within one frame period, and deliverDelayedTouchEvent() is
called from flushFrameSynchronousEvents() to send that single event. This
is the reason why touch compression lives here so far, instead of in a
lower layer: the render loop updates the scene in sync with the screen's
vsync, and flushFrameSynchronousEvents() is called from there (for example
from QSGThreadedRenderLoop::polishAndSync(), and equivalent places in other
render loops). It would be preferable to move this code down to a lower
level eventually, though, because it's not fundamentally a Qt Quick concern.
This optimization can be turned off by setting the environment variable
\c QML_NO_TOUCH_COMPRESSION.
Returns \c true if "done", \c false if the caller needs to finish the
\a event delivery.
*/
bool QQuickDeliveryAgentPrivate::compressTouchEvent(QTouchEvent *event)
{
// If this is a subscene agent, don't store any events, because
// flushFrameSynchronousEvents() is only called on the window's DA.
if (isSubsceneAgent)
return false;
QEventPoint::States states = event->touchPointStates();
if (states.testFlag(QEventPoint::State::Pressed) || states.testFlag(QEventPoint::State::Released)) {
qCDebug(lcTouchCmprs) << "no compression" << event;
// we can only compress an event that doesn't include any pressed or released points
return false;
}
if (!delayedTouch) {
delayedTouch.reset(new QMutableTouchEvent(event->type(), event->pointingDevice(), event->modifiers(), event->points()));
delayedTouch->setTimestamp(event->timestamp());
for (qsizetype i = 0; i < delayedTouch->pointCount(); ++i) {
auto &tp = delayedTouch->point(i);
QMutableEventPoint::detach(tp);
}
++compressedTouchCount;
qCDebug(lcTouchCmprs) << "delayed" << compressedTouchCount << delayedTouch.get();
if (QQuickWindow *window = rootItem->window())
window->maybeUpdate();
return true;
}
// check if this looks like the last touch event
if (delayedTouch->type() == event->type() &&
delayedTouch->device() == event->device() &&
delayedTouch->modifiers() == event->modifiers() &&
delayedTouch->pointCount() == event->pointCount())
{
// possible match.. is it really the same?
bool mismatch = false;
auto tpts = event->points();
for (qsizetype i = 0; i < event->pointCount(); ++i) {
const auto &tp = tpts.at(i);
const auto &tpDelayed = delayedTouch->point(i);
if (tp.id() != tpDelayed.id()) {
mismatch = true;
break;
}
if (tpDelayed.state() == QEventPoint::State::Updated && tp.state() == QEventPoint::State::Stationary)
QMutableEventPoint::setState(tpts[i], QEventPoint::State::Updated);
}
// matching touch event? then give delayedTouch a merged set of touchpoints
if (!mismatch) {
// have to create a new event because QMutableTouchEvent::setTouchPoints() is missing
// TODO optimize, or move event compression elsewhere
delayedTouch.reset(new QMutableTouchEvent(event->type(), event->pointingDevice(), event->modifiers(), tpts));
delayedTouch->setTimestamp(event->timestamp());
for (qsizetype i = 0; i < delayedTouch->pointCount(); ++i) {
auto &tp = delayedTouch->point(i);
QMutableEventPoint::detach(tp);
}
++compressedTouchCount;
qCDebug(lcTouchCmprs) << "coalesced" << compressedTouchCount << delayedTouch.get();
if (QQuickWindow *window = rootItem->window())
window->maybeUpdate();
return true;
}
}
// merging wasn't possible, so deliver the delayed event first, and then delay this one
deliverDelayedTouchEvent();
delayedTouch.reset(new QMutableTouchEvent(event->type(), event->pointingDevice(),
event->modifiers(), event->points()));
delayedTouch->setTimestamp(event->timestamp());
return true;
}
/*!
\internal
Returns \c true if \a event ends a finger touch sequence: that is,
of one or more touchpoints, every point has been released,
and we are sure that they came from fingers, not synthesized.
Finger touch has no hover state of its own (until we support hover-detecting touchscreens).
Stylus-proximity hover must not be cleared here.
This mirrors the rule in QQuickHoverHandler::handleEventPoint().
*/
static bool isEndOfFingerTouchSequence(const QTouchEvent *event)
{
if (!event->isEndEvent())
return false;
if (!event->pointCount())
return false;
// in case of AA_SynthesizeTouchForUnhandledMouseEvents, for example (unusual)
if (Q_UNLIKELY(event->pointerType() != QPointingDevice::PointerType::Finger))
return false;
// Ask the device to be sure, since some don't report every still-down contact in every event:
// e.g. the Stationary points that QWindowsPointerHandler::translateTouchEvent()
// synthesizes for digitizers that send one message per finger.
const auto *devPriv = QPointingDevicePrivate::get(event->pointingDevice());
for (const auto &activePoint : devPriv->activePoints) {
if (activePoint.second.eventPoint.state() != QEventPoint::State::Released)
return false;
}
return true;
}
// entry point for touch event delivery:
// - translate the event to window coordinates
// - compress the event instead of delivering it if applicable
// - call deliverTouchPoints to actually dispatch the points
void QQuickDeliveryAgentPrivate::handleTouchEvent(QTouchEvent *event)
{
Q_Q(QQuickDeliveryAgent);
translateTouchEvent(event);
// TODO remove: touch and mouse should be independent until we come to touch->mouse synth
if (event->pointCount() && !isTouchPadWithMouseEmulation(event->device())) {
auto &point = event->point(0);
if (point.state() == QEventPoint::State::Released) {
// Clearing lastMousePosition here switches off the frame-synchronous hover delivery
// in flushFrameSynchronousEvents(), which would otherwise notice that
// hoverItems holds entries with stale hoverIds. Hence clearTouchHover() below.
lastMousePosition = QPointF();
lastMousePositionFromTouch = false;
} else {
lastMousePosition = point.position();
lastMousePositionFromTouch =
event->pointerType() == QPointingDevice::PointerType::Finger;
}
}
// After delivery of the last touchpoint release, hover that the fingertips established
// has to go with it. It's not the platform plugin's job to synthesize a LeaveEvent
// on touch release. And not every device has a mouse to change hover state, either.
const bool fingerHoverEnded = isEndOfFingerTouchSequence(event);
qCDebug(lcTouch) << q << event;
static bool qquickwindow_no_touch_compression = qEnvironmentVariableIsSet("QML_NO_TOUCH_COMPRESSION");
if (qquickwindow_no_touch_compression || pointerEventRecursionGuard) {
deliverPointerEvent(event);
} else if (!compressTouchEvent(event)) {
if (delayedTouch) {
deliverDelayedTouchEvent();
qCDebug(lcTouchCmprs) << "resuming delivery" << event;
}
deliverPointerEvent(event);
}
// compressTouchEvent() never compresses an event that carries a released point,
// so an ended sequence is always delivered by one of the branches above first.
if (fingerHoverEnded)
clearTouchHover(event->timestamp());
}
/*!
Handle \a event on behalf of this delivery agent's window or subscene.
*/
void QQuickDeliveryAgentPrivate::handleMouseEvent(QMouseEvent *event)
{
Q_Q(QQuickDeliveryAgent);
// We generally don't want OS-synthesized mouse events, because Qt Quick does its own touch->mouse synthesis.
// But if the platform converts long-press to right-click, it's ok to react to that,
// unless the user has opted out by setting QT_QUICK_ALLOW_SYNTHETIC_RIGHT_CLICK=0.
if (event->source() == Qt::MouseEventSynthesizedBySystem &&
!(event->button() == Qt::RightButton && allowSyntheticRightClick())) {
event->accept();
return;
}
qCDebug(lcMouse) << q << event;
switch (event->type()) {
case QEvent::MouseButtonPress:
Q_QUICK_INPUT_PROFILE(QQuickProfiler::Mouse, QQuickProfiler::InputMousePress, event->button(),
event->buttons());
deliverPointerEvent(event);
break;
case QEvent::MouseButtonRelease:
Q_QUICK_INPUT_PROFILE(QQuickProfiler::Mouse, QQuickProfiler::InputMouseRelease, event->button(),
event->buttons());
deliverPointerEvent(event);
#if QT_CONFIG(cursor)
QQuickWindowPrivate::get(rootItem->window())->updateCursor(event->scenePosition());
#endif
break;
case QEvent::MouseButtonDblClick:
Q_QUICK_INPUT_PROFILE(QQuickProfiler::Mouse, QQuickProfiler::InputMouseDoubleClick,
event->button(), event->buttons());
deliverPointerEvent(event);
break;
case QEvent::MouseMove: {
Q_QUICK_INPUT_PROFILE(QQuickProfiler::Mouse, QQuickProfiler::InputMouseMove,
event->position().x(), event->position().y());
const QPointF last = lastMousePosition.isNull() ? event->scenePosition() : lastMousePosition;
lastMousePosition = event->scenePosition();
// A real mouse move takes the position back from any fingertip that had claimed
// it, so hover established from here on belongs to the cursor.
lastMousePositionFromTouch = false;
qCDebug(lcHoverTrace) << q << "mouse pos" << last << "->" << lastMousePosition;
if (!event->points().size() || !event->exclusiveGrabber(event->point(0))) {
bool accepted = deliverHoverEvent(event->scenePosition(), last, event->modifiers(), event->timestamp());
event->setAccepted(accepted);
}
deliverPointerEvent(event);
#if QT_CONFIG(cursor)
// The pointer event could result in a cursor change (reaction), so update it afterwards.
QQuickWindowPrivate::get(rootItem->window())->updateCursor(event->scenePosition());
#endif
break;
}
default:
Q_ASSERT(false);
break;
}
}
/*! \internal
Flush events before a frame is rendered in \a win.
This is here because of compressTouchEvent(): we need to ensure that
coalesced touch events are actually delivered in time to cause the desired
reactions of items and their handlers. And then since it was introduced
because of that, we started using this function for once-per-frame hover
events too, to take care of changing hover state when an item animates
under the mouse cursor at a time that the mouse cursor is not moving.
This is done before QQuickItem::updatePolish() is called on all the items
that requested polishing.
*/
void QQuickDeliveryAgentPrivate::flushFrameSynchronousEvents(QQuickWindow *win)
{
Q_Q(QQuickDeliveryAgent);
QQuickDeliveryAgent *deliveringAgent = QQuickDeliveryAgentPrivate::currentEventDeliveryAgent;
QQuickDeliveryAgentPrivate::currentEventDeliveryAgent = q;
if (delayedTouch) {
deliverDelayedTouchEvent();
// Touch events which constantly start animations (such as a behavior tracking
// the mouse point) need animations to start.
QQmlAnimationTimer *ut = QQmlAnimationTimer::instance();
if (ut && ut->hasStartAnimationPending())
ut->startAnimations();
}
// In webOS we already have the alternative to the issue that this
// wanted to address and thus skipping this part won't break anything.
#if !defined(Q_OS_WEBOS)
// Periodically, if any items are dirty, send a synthetic hover,
// in case items have changed position, visibility, etc.
// For instance, during animation (including the case of a ListView
// whose delegates contain MouseAreas), a MouseArea needs to know
// whether it has moved into a position where it is now under the cursor.
// We do this once per frame if frameSynchronousHoverInterval == 0, or
// skip some frames until elapsed time > frameSynchronousHoverInterval,
// or skip it altogether if frameSynchronousHoverInterval < 0.
// TODO do this for each known mouse device or come up with a different strategy
if (frameSynchronousHoverInterval >= 0) {
const bool timerActive = frameSynchronousHoverInterval > 0;
const bool timerMature = frameSynchronousHoverTimer.elapsed() >= frameSynchronousHoverInterval;
if (timerActive && !timerMature) {
qCDebug(lcHoverTrace) << q << "frame-sync hover delivery delayed: elapsed"
<< frameSynchronousHoverTimer.elapsed() << "<" << frameSynchronousHoverInterval;
if (!frameSynchronousDelayTimer.isActive())
frameSynchronousDelayTimer.start(frameSynchronousHoverInterval - frameSynchronousHoverTimer.elapsed(), q);
} else if (!win->mouseGrabberItem() && !lastMousePosition.isNull() &&
(timerMature || QQuickWindowPrivate::get(win)->dirtyItemList)) {
frameSynchronousDelayTimer.stop();
qCDebug(lcHoverTrace) << q << "delivering frame-sync hover to root @" << lastMousePosition
<< "after elapsed time" << frameSynchronousHoverTimer.elapsed();
if (deliverHoverEvent(lastMousePosition, lastMousePosition, QGuiApplication::keyboardModifiers(), 0)) {
#if QT_CONFIG(cursor)
QQuickWindowPrivate::get(rootItem->window())->updateCursor(
sceneTransform ? sceneTransform->map(lastMousePosition) : lastMousePosition, rootItem);
#endif
}
if (timerActive)
frameSynchronousHoverTimer.restart();
++frameSynchronousHover_counter;
qCDebug(lcHoverTrace) << q << "frame-sync hover delivery done: round" << frameSynchronousHover_counter;
}
}
#else
Q_UNUSED(win);
#endif
if (Q_UNLIKELY(QQuickDeliveryAgentPrivate::currentEventDeliveryAgent &&
QQuickDeliveryAgentPrivate::currentEventDeliveryAgent != q))
qCWarning(lcPtr, "detected interleaved frame-sync and actual events");
QQuickDeliveryAgentPrivate::currentEventDeliveryAgent = deliveringAgent;
}
/*! \internal
React to the fact that \a grabber underwent a grab \a transition
while an item or handler was handling \a point from \a event.
I.e. handle the QPointingDevice::grabChanged() signal.
This notifies the relevant items and/or pointer handlers, and
does cleanup when grabs are lost or relinquished.
*/
void QQuickDeliveryAgentPrivate::onGrabChanged(QObject *grabber, QPointingDevice::GrabTransition transition,
const QPointerEvent *event, const QEventPoint &point)
{
Q_Q(QQuickDeliveryAgent);
const bool grabGained = (transition == QPointingDevice::GrabTransition::GrabExclusive ||
transition == QPointingDevice::GrabTransition::GrabPassive);
// note: event can be null, if the signal was emitted from QPointingDevicePrivate::removeGrabber(grabber)
if (auto *handler = qmlobject_cast<QQuickPointerHandler *>(grabber)) {
if (handler->parentItem()) {
auto itemPriv = QQuickItemPrivate::get(handler->parentItem());
if (itemPriv->deliveryAgent() == q) {
if (grabGained) {
handler->onGrabChanged(handler, transition, const_cast<QPointerEvent *>(event),
const_cast<QEventPoint &>(point));
} else {
// When a grab is relinquished/lost, the event point is localized to the new grabber
// or not localized at all (QTBUG-146781, QTBUG-147003). Thus, we must re-localize it
// for the old grabber's onGrabChanged() handler.
QEventPoint lostPoint(point);
QMutableEventPoint::setPosition(lostPoint, handler->parentItem()->mapFromScene(point.scenePosition()));
handler->onGrabChanged(handler, transition, const_cast<QPointerEvent *>(event),
lostPoint);
qCDebug(lcPtrLoc) << event->type() << "@" << point.scenePosition()
<< "to old handler" << grabber << "->" << lostPoint;
}
}
if (grabGained) {
// An item that is NOT a subscene root needs to track whether it got a grab via a subscene delivery agent,
// whereas the subscene root item already knows it has its own DA.
if (isSubsceneAgent && (!itemPriv->extra.isAllocated() || !itemPriv->extra->subsceneDeliveryAgent))
itemPriv->maybeHasSubsceneDeliveryAgent = true;
}
} else if (!isSubsceneAgent) {
handler->onGrabChanged(handler, transition, const_cast<QPointerEvent *>(event),
const_cast<QEventPoint &>(point));
}
} else if (auto *grabberItem = qmlobject_cast<QQuickItem *>(grabber)) {
switch (transition) {
case QPointingDevice::CancelGrabExclusive:
case QPointingDevice::UngrabExclusive:
if (isDeliveringTouchAsMouse() || isSinglePointDevice(point.device())) {
// If an EventPoint from the mouse or the synth-mouse or from any
// mouse-like device is ungrabbed, call QQuickItem::mouseUngrabEvent().
QMutableSinglePointEvent e(QEvent::UngrabMouse, point.device(), point);
hasFiltered.clear();
if (!sendFilteredMouseEvent(&e, grabberItem, grabberItem->parentItem())) {
lastUngrabbed = grabberItem;
grabberItem->mouseUngrabEvent();
}
} else {
// Multi-point event: call QQuickItem::touchUngrabEvent() only if
// all eventpoints are released or cancelled.
bool allReleasedOrCancelled = true;
if (transition == QPointingDevice::UngrabExclusive && event) {
for (const auto &pt : event->points()) {
if (pt.state() != QEventPoint::State::Released) {
allReleasedOrCancelled = false;
break;
}
}
}
if (allReleasedOrCancelled)
grabberItem->touchUngrabEvent();
}
break;
default:
break;
}
auto *itemPriv = QQuickItemPrivate::get(grabberItem);
// An item that is NOT a subscene root needs to track whether it got a grab via a subscene delivery agent,
// whereas the subscene root item already knows it has its own DA.
if (isSubsceneAgent && grabGained && (!itemPriv->extra.isAllocated() || !itemPriv->extra->subsceneDeliveryAgent))
itemPriv->maybeHasSubsceneDeliveryAgent = true;
}
if (currentEventDeliveryAgent == q && event && event->device()) {
switch (transition) {
case QPointingDevice::GrabPassive: {
auto epd = QPointingDevicePrivate::get(const_cast<QPointingDevice*>(event->pointingDevice()))->queryPointById(point.id());
Q_ASSERT(epd);
QPointingDevicePrivate::setPassiveGrabberContext(epd, grabber, q);
qCDebug(lcPtr) << "remembering that" << q << "handles point" << point.id() << "after" << transition;
} break;
case QPointingDevice::GrabExclusive: {
auto epd = QPointingDevicePrivate::get(const_cast<QPointingDevice*>(event->pointingDevice()))->queryPointById(point.id());
Q_ASSERT(epd);
epd->exclusiveGrabberContext = q;
qCDebug(lcPtr) << "remembering that" << q << "handles point" << point.id() << "after" << transition;
} break;
case QPointingDevice::CancelGrabExclusive:
case QPointingDevice::UngrabExclusive:
// taken care of in QPointingDevicePrivate::setExclusiveGrabber(,,nullptr), removeExclusiveGrabber()
break;
case QPointingDevice::UngrabPassive:
case QPointingDevice::CancelGrabPassive:
// taken care of in QPointingDevicePrivate::removePassiveGrabber(), clearPassiveGrabbers()
break;
case QPointingDevice::OverrideGrabPassive:
// not in use at this time
break;
}
}
}
/*! \internal
Called when a QPointingDevice is detected, to ensure that the
QPointingDevice::grabChanged() signal is connected to
QQuickDeliveryAgentPrivate::onGrabChanged().
\c knownPointingDevices is maintained only to track signal connections, and
should not be used for other purposes. The usual place to get a list of all
devices is QInputDevice::devices().
*/
void QQuickDeliveryAgentPrivate::ensureDeviceConnected(const QPointingDevice *dev)
{
Q_Q(QQuickDeliveryAgent);
if (knownPointingDevices.contains(dev))
return;
knownPointingDevices.append(dev);
connect(dev, &QPointingDevice::grabChanged, this, &QQuickDeliveryAgentPrivate::onGrabChanged);
QObject::connect(dev, &QObject::destroyed, q, [this, dev] {this->knownPointingDevices.removeAll(dev);});
}
/*! \internal
The entry point for delivery of \a event after determining that it \e is a
pointer event, and either does not need to be coalesced in
compressTouchEvent(), or already has been.
When it returns, event delivery is done.
*/
void QQuickDeliveryAgentPrivate::deliverPointerEvent(QPointerEvent *event)
{
Q_Q(QQuickDeliveryAgent);
if (isTabletEvent(event))
qCDebug(lcTablet) << q << event;
// If users spin the eventloop as a result of event delivery, we disable
// event compression and send events directly. This is because we consider
// the usecase a bit evil, but we at least don't want to lose events.
++pointerEventRecursionGuard;
eventsInDelivery.push(event);
// So far this is for use in Qt Quick 3D: if a QEventPoint is grabbed,
// updates get delivered here pretty directly, bypassing picking; but we need to
// be able to map the 2D viewport coordinate to a 2D coordinate within
// d->rootItem, a 2D scene that has been arbitrarily mapped onto a 3D object.
QVarLengthArray<QPointF, 16> originalScenePositions;
if (sceneTransform) {
originalScenePositions.resize(event->pointCount());
for (int i = 0; i < event->pointCount(); ++i) {
auto &pt = event->point(i);
originalScenePositions[i] = pt.scenePosition();
QMutableEventPoint::setScenePosition(pt, sceneTransform->map(pt.scenePosition()));
qCDebug(lcPtrLoc) << q << event->type() << pt.id() << "transformed scene pos" << pt.scenePosition();
}
} else if (isSubsceneAgent) {
qCDebug(lcPtrLoc) << q << event->type() << "no scene transform set";
}
skipDelivery.clear();
QQuickPointerHandlerPrivate::deviceDeliveryTargets(event->pointingDevice()).clear();
if (sceneTransform)
qCDebug(lcPtr) << q << "delivering with" << sceneTransform << event;
else
qCDebug(lcPtr) << q << "delivering" << event;
for (int i = 0; i < event->pointCount(); ++i)
event->point(i).setAccepted(false);
if (event->isBeginEvent()) {
ensureDeviceConnected(event->pointingDevice());
if (event->type() == QEvent::MouseButtonPress && rootItem->window()
&& static_cast<QSinglePointEvent *>(event)->button() == Qt::RightButton) {
QQuickWindowPrivate::get(rootItem->window())->rmbContextMenuEventEnabled = true;
}
if (!deliverPressOrReleaseEvent(event))
event->setAccepted(false);
}
auto isHoveringMoveEvent = [](QPointerEvent *event) -> bool {
if (event->type() == QEvent::MouseMove) {
const auto *spe = static_cast<const QSinglePointEvent *>(event);
if (spe->button() == Qt::NoButton && spe->buttons() == Qt::NoButton)
return true;
}
return false;
};
/*
If some QEventPoints were not yet handled, deliver to existing grabbers,
and then non-grabbing pointer handlers.
But don't deliver stray mouse moves in which no buttons are pressed:
stray mouse moves risk deactivating handlers that don't expect them;
for mouse hover tracking, we rather use deliverHoverEvent().
But do deliver TabletMove events, in case there is a HoverHandler that
changes its cursorShape depending on stylus type.
*/
if (!allUpdatedPointsAccepted(event) && !isHoveringMoveEvent(event))
deliverUpdatedPoints(event);
if (event->isEndEvent())
deliverPressOrReleaseEvent(event, true);
// failsafe: never allow touch->mouse synthesis to persist after all touchpoints are released,
// or after the touchmouse is released
if (isTouchEvent(event) && touchMouseId >= 0) {
if (static_cast<QTouchEvent *>(event)->touchPointStates() == QEventPoint::State::Released) {
cancelTouchMouseSynthesis();
} else {
auto touchMousePoint = event->pointById(touchMouseId);
if (touchMousePoint && touchMousePoint->state() == QEventPoint::State::Released)
cancelTouchMouseSynthesis();
}
}
eventsInDelivery.pop();
if (sceneTransform) {
for (int i = 0; i < event->pointCount(); ++i)
QMutableEventPoint::setScenePosition(event->point(i), originalScenePositions.at(i));
}
--pointerEventRecursionGuard;
lastUngrabbed = nullptr;
}
/*! \internal
Returns a list of all items that are spatially relevant to receive \a event
occurring at \a scenePos, starting with \a item and recursively
checking all the children.
\a localPos is the same as \a scenePos mapped to \a item (given as an
optimization, to avoid mapping it again). If \a pointId is given (if
pointId >= 0), the event is a QPointerEvent: so the expectation is that
this function must map the position to each child, during recursion.
The reason we need to do it is that \a predicate may expect the QEventPoint
to be localized already. eventTargets() is able to do the mapping using
only QQuickItemPrivate::itemToParentTransform(), which is cheaper than
calling windowToItemTransform() at each step.
\a event could alternatively be a QContextMenuEvent: then there is no
QEventPoint available, so pointId is given as -1 to indicate that
this function does \e not have responsibility to remap it to each child.
\list
\li If QQuickItemPrivate::effectivelyClipsEventHandlingChildren() is
\c true \e and \a scenePos is outside of QQuickItem::clipRect(), and
\a item is \e not the root item, its children are also omitted.
(We stop the recursion, because any clipped-off portions of children
under \a scenePos are invisible; or, because we know that all children
are fully inside the parent.)
\li Ignore any item in a subscene that "belongs to" a different
DeliveryAgent. (In current practice, this only happens in 2D scenes in
Qt Quick 3D.)
\li Ignore any item for which the given \a predicate returns \c false;
include any item for which the predicate returns \c true.
\endlist
\note If \c {QQuickView::resizeMode() == SizeViewToRootObject} (the default),
the root item might not fill the window: so we don't check
effectivelyClipsEventHandlingChildren() on it. It could even be 0 x 0 if
width and height aren't declared.)
*/
QList<QQuickItem *> QQuickDeliveryAgentPrivate::eventTargets(QQuickItem *item, const QEvent *event, int pointId,
QPointF localPos, QPointF scenePos, qxp::function_ref<std::optional<bool> (QQuickItem *, const QEvent *)> predicate) const
{
QList<QQuickItem *> targets;
eventTargetsAppend(item, event, pointId, localPos, scenePos, predicate, targets);
return targets;
}
/*!
\internal
Recursively collects event-target items into \a targets in reverse
paint order (highest stacking order first), interleaving \a item
among its children according to z-order.
*/
// FIXME: should this be iterative instead of recursive?
void QQuickDeliveryAgentPrivate::eventTargetsAppend(QQuickItem *item, const QEvent *event, int pointId,
QPointF localPos, QPointF scenePos, qxp::function_ref<std::optional<bool> (QQuickItem *, const QEvent *)> predicate,
QList<QQuickItem *> &targets) const
{
auto itemPrivate = QQuickItemPrivate::get(item);
// If the item clips, or all children are inside, and it's not the root item,
// and scenePos is outside its rectangular bounds, we can skip this item
// and all its children, to save time. (This check is performance-sensitive!)
if (item != rootItem && !itemPrivate->eventHandlingBounds().contains(localPos) &&
itemPrivate->effectivelyClipsEventHandlingChildren()) {
qCDebug(lcPtrLoc) << "skipping because" << localPos << "is outside rectangular bounds of" << item;
return;
}
// If we didn't return early: check containment more thoroughly, then build
// a list of children in paint order, modified to respect z property adjustments.
const QList<QQuickItem *> children = itemPrivate->paintOrderChildItems();
if (pointId >= 0) {
// If pointId is set, it's meant to indicate that this is a QPointerEvent, not e.g. a QContextMenuEvent.
// Localize the relevant QEventPoint before calling the predicate: if it calls anyPointerHandlerWants(),
// position() must be correct.
Q_ASSERT(event->isPointerEvent());
QPointerEvent *pev = const_cast<QPointerEvent *>(static_cast<const QPointerEvent *>(event));
QEventPoint *point = pev->pointById(pointId);
Q_ASSERT(point);
QMutableEventPoint::setPosition(*point, localPos);
}
const std::optional<bool> override = predicate(item, event);
const bool relevant = override.has_value() ? override.value()
: item == rootItem || item->contains(localPos);
// Iterate in reverse paint order (highest stacking order first).
// Emit self before the first child with z < 0 (i.e. between children
// painted above and below self).
bool selfEmitted = !relevant;
for (int ii = children.size() - 1; ii >= 0; --ii) {
QQuickItem *child = children.at(ii);
if (!selfEmitted && child->z() < 0) {
targets.append(item);
selfEmitted = true;
}
auto childPrivate = QQuickItemPrivate::get(child);
if (!child->isVisible() || !child->isEnabled() || childPrivate->culled ||
(childPrivate->extra.isAllocated() && childPrivate->extra->subsceneDeliveryAgent))
continue;
QTransform childToParent;
childPrivate->itemToParentTransform(&childToParent);
const QPointF childLocalPos = childToParent.inverted().map(localPos);
eventTargetsAppend(child, event, pointId, childLocalPos, scenePos, predicate, targets);
}
// If all children have z >= 0, self goes last in reverse order
if (!selfEmitted)
targets.append(item);
}
/*! \internal
Returns a list of all items that are spatially relevant to receive \a event
occurring at \a point, starting with \a item and recursively checking all
the children.
\list
\li If an item has pointer handlers, call
QQuickPointerHandler::wantsEventPoint()
on every handler to decide whether the item is eligible.
\li Otherwise, if \a checkMouseButtons is \c true, it means we are
finding targets for a mouse event, so no item for which
acceptedMouseButtons() is NoButton will be added.
\li Otherwise, if \a checkAcceptsTouch is \c true, it means we are
finding targets for a touch event, so either acceptTouchEvents() must
return true \e or it must accept a synthesized mouse event. I.e. if
acceptTouchEvents() returns false, it gets added only if
acceptedMouseButtons() is true.
\li If QQuickItem::clip() is \c true \e and the \a point is outside of
QQuickItem::clipRect(), its children are also omitted. (We stop the
recursion, because any clipped-off portions of children under \a point
are invisible.)
\li Ignore any item in a subscene that "belongs to" a different
DeliveryAgent. (In current practice, this only happens in 2D scenes in
Qt Quick 3D.)
\endlist
The list returned from this function is the list of items that will be
"visited" when delivering any event for which QPointerEvent::isBeginEvent()
is \c true.
*/
QList<QQuickItem *> QQuickDeliveryAgentPrivate::pointerTargets(QQuickItem *item, const QPointerEvent *event, const QEventPoint &point,
bool checkMouseButtons, bool checkAcceptsTouch) const
{
auto predicate = [point, checkMouseButtons, checkAcceptsTouch](QQuickItem *item, const QEvent *ev) -> std::optional<bool> {
const QPointerEvent *event = static_cast<const QPointerEvent *>(ev);
auto itemPrivate = QQuickItemPrivate::get(item);
if (itemPrivate->hasPointerHandlers()) {
if (itemPrivate->anyPointerHandlerWants(event, point))
return true;
} else {
if (checkMouseButtons && item->acceptedMouseButtons() == Qt::NoButton)
return false;
if (checkAcceptsTouch && !(item->acceptTouchEvents() || item->acceptedMouseButtons()))
return false;
}
return std::nullopt;
};
return eventTargets(item, event, point.id(), item->mapFromScene(point.scenePosition()), point.scenePosition(), predicate);
}
/*! \internal
Returns a joined list consisting of the items in \a list1 and \a list2.
\a list1 has priority; common items come last.
*/
QList<QQuickItem *> QQuickDeliveryAgentPrivate::mergePointerTargets(const QList<QQuickItem *> &list1, const QList<QQuickItem *> &list2) const
{
QList<QQuickItem *> targets = list1;
// start at the end of list2
// if item not in list, append it
// if item found, move to next one, inserting before the last found one
int insertPosition = targets.size();
for (int i = list2.size() - 1; i >= 0; --i) {
int newInsertPosition = targets.lastIndexOf(list2.at(i), insertPosition);
if (newInsertPosition >= 0) {
Q_ASSERT(newInsertPosition <= insertPosition);
insertPosition = newInsertPosition;
}
// check for duplicates, only insert if the item isn't there already
if (insertPosition == targets.size() || list2.at(i) != targets.at(insertPosition))
targets.insert(insertPosition, list2.at(i));
}
return targets;
}
/*! \internal
Deliver updated points to existing grabbers.
*/
void QQuickDeliveryAgentPrivate::deliverUpdatedPoints(QPointerEvent *event)
{
Q_Q(const QQuickDeliveryAgent);
bool done = false;
const auto grabbers = exclusiveGrabbers(event);
hasFiltered.clear();
for (auto grabber : grabbers) {
// The grabber is guaranteed to be either an item or a handler.
QQuickItem *receiver = qmlobject_cast<QQuickItem *>(grabber);
if (!receiver) {
// The grabber is not an item? It's a handler then. Let it have the event first.
QQuickPointerHandler *handler = static_cast<QQuickPointerHandler *>(grabber);
receiver = static_cast<QQuickPointerHandler *>(grabber)->parentItem();
// Filtering via QQuickItem::childMouseEventFilter() is only possible
// if the handler's parent is an Item. It could be a QQ3D object.
if (receiver) {
hasFiltered.clear();
if (sendFilteredPointerEvent(event, receiver))
done = true;
localizePointerEvent(event, receiver);
}
handler->handlePointerEvent(event);
}
if (done)
break;
// If the grabber is an item or the grabbing handler didn't handle it,
// then deliver the event to the item (which may have multiple handlers).
hasFiltered.clear();
if (receiver)
deliverMatchingPointsToItem(receiver, true, event);
}
// Deliver to each eventpoint's passive grabbers (but don't visit any handler more than once)
for (auto &point : event->points()) {
auto epd = QPointingDevicePrivate::get(event->pointingDevice())->queryPointById(point.id());
if (Q_UNLIKELY(!epd)) {
qWarning() << "point is not in activePoints" << point;
continue;
}
QList<QPointer<QObject>> relevantPassiveGrabbers;
for (int i = 0; i < epd->passiveGrabbersContext.size(); ++i) {
if (epd->passiveGrabbersContext.at(i).data() == q)
relevantPassiveGrabbers << epd->passiveGrabbers.at(i);
}
if (!relevantPassiveGrabbers.isEmpty())
deliverToPassiveGrabbers(relevantPassiveGrabbers, event);
// Ensure that HoverHandlers are updated, in case no items got dirty so far and there's no update request
if (event->type() == QEvent::TouchUpdate && !isTouchPadWithMouseEmulation(event->device())) {
for (const auto &[item, id, fromFinger] : hoverItems) {
if (item) {
bool res = deliverHoverEventToItem(item, item->mapFromScene(point.scenePosition()), point.scenePosition(), point.sceneLastPosition(),
point.globalPosition(), event->modifiers(), event->timestamp(), HoverChange::Set);
// if the event was accepted, then the item's ID must be valid
Q_ASSERT(([this, item = item.get(), res]{
const auto it2 = findHoverStateByItem(std::as_const(hoverItems), item);
return !res || it2->hoverId != 0;
}()));
}
}
}
}
if (done)
return;
// If some points weren't grabbed, deliver only to non-grabber PointerHandlers in reverse paint order
if (!allPointsGrabbed(event)) {
QList<QQuickItem *> targetItems;
for (auto &point : event->points()) {
// Presses were delivered earlier; not the responsibility of deliverUpdatedTouchPoints.
// Don't find handlers for points that are already grabbed by an Item (such as Flickable).
if (point.state() == QEventPoint::Pressed || qmlobject_cast<QQuickItem *>(event->exclusiveGrabber(point)))
continue;
QList<QQuickItem *> targetItemsForPoint = pointerTargets(rootItem, event, point, false, false);
if (targetItems.size()) {
targetItems = mergePointerTargets(targetItems, targetItemsForPoint);
} else {
targetItems = targetItemsForPoint;
}
}
for (QQuickItem *item : targetItems) {
if (grabbers.contains(item))
continue;
QQuickItemPrivate *itemPrivate = QQuickItemPrivate::get(item);
localizePointerEvent(event, item);
itemPrivate->handlePointerEvent(event, true); // avoid re-delivering to grabbers
if (allPointsGrabbed(event))
break;
}
}
}
/*! \internal
Deliver a pointer \a event containing newly pressed or released QEventPoints.
If \a handlersOnly is \c true, skip the items and just deliver to Pointer Handlers
(via QQuickItemPrivate::handlePointerEvent()).
For the sake of determinism, this function first builds the list
\c targetItems by calling pointerTargets() on the root item. That is, the
list of items to "visit" is determined at the beginning, and will not be
affected if items reparent, hide, or otherwise try to make themselves
eligible or ineligible during delivery. (Avoid bugs due to ugly
just-in-time tricks in JS event handlers, filters etc.)
Whenever a touch gesture is in progress, and another touchpoint is pressed,
or an existing touchpoint is released, we "start over" with delivery:
that's why this function is called whenever the event \e contains newly
pressed or released points. It's not necessary for a handler or an item to
greedily grab all touchpoints just in case a valid gesture might start.
QQuickMultiPointHandler::wantsPointerEvent() can calmly return \c false if
the number of points is less than QQuickMultiPointHandler::minimumPointCount(),
because it knows it will be asked again if the number of points increases.
When \a handlersOnly is \c false, \a event visits the items in \c targetItems
via QQuickItem::event(). We have to call sendFilteredPointerEvent()
before visiting each item, just in case a Flickable (or some other
parent-filter) will decide to intercept the event. But we also have to be
very careful never to let the same Flickable filter the same event twice,
because when Flickable decides to intercept, it lets the child item have
that event, and then grabs the next event. That allows you to drag a
Slider, DragHandler or whatever inside a ListView delegate: if you're
dragging in the correct direction for the draggable child, it can use
QQuickItem::setKeepMouseGrab(), QQuickItem::setKeepTouchGrab() or
QQuickPointerHandler::grabPermissions() to prevent Flickable from
intercepting during filtering, only if it actually \e has the exclusive
grab already when Flickable attempts to take it. Typically, both the
Flickable and the child are checking the same drag threshold, so the
child must have a chance to grab and \e keep the grab before Flickable
gets a chance to steal it, even though Flickable actually sees the
event first during filtering.
*/
bool QQuickDeliveryAgentPrivate::deliverPressOrReleaseEvent(QPointerEvent *event, bool handlersOnly)
{
QList<QQuickItem *> targetItems;
const bool isTouch = isTouchEvent(event);
if (isTouch && event->isBeginEvent() && isDeliveringTouchAsMouse()) {
if (auto point = const_cast<QPointingDevicePrivate *>(QPointingDevicePrivate::get(touchMouseDevice))->queryPointById(touchMouseId)) {
// When a second point is pressed, if the first point's existing
// grabber was a pointer handler while a filtering parent is filtering
// the same first point _as mouse_: we're starting over with delivery,
// so we need to allow the second point to now be sent as a synth-mouse
// instead of the first one, so that filtering parents (maybe even the
// same one) can get a chance to see the second touchpoint as a
// synth-mouse and perhaps grab it. Ideally we would always do this
// when a new touchpoint is pressed, but this compromise fixes
// QTBUG-70998 and avoids breaking tst_FlickableInterop::touchDragSliderAndFlickable
if (qobject_cast<QQuickPointerHandler *>(event->exclusiveGrabber(point->eventPoint)))
cancelTouchMouseSynthesis();
} else {
qCWarning(lcTouchTarget) << "during delivery of touch press, synth-mouse ID" << Qt::hex << touchMouseId << "is missing from" << event;
}
}
for (int i = 0; i < event->pointCount(); ++i) {
auto &point = event->point(i);
// Regardless whether a touchpoint could later result in a synth-mouse event:
// if the double-tap time or space constraint has been violated,
// reset state to prevent a double-click event.
if (isTouch && point.state() == QEventPoint::Pressed)
resetIfDoubleTapPrevented(point);
QList<QQuickItem *> targetItemsForPoint = pointerTargets(rootItem, event, point, !isTouch, isTouch);
if (targetItems.size()) {
targetItems = mergePointerTargets(targetItems, targetItemsForPoint);
} else {
targetItems = targetItemsForPoint;
}
}
QList<QPointer<QQuickItem>> safeTargetItems(targetItems.begin(), targetItems.end());
for (auto &item : safeTargetItems) {
if (item.isNull())
continue;
// failsafe: when items get into a subscene somehow, ensure that QQuickItemPrivate::deliveryAgent() can find it
if (isSubsceneAgent)
QQuickItemPrivate::get(item)->maybeHasSubsceneDeliveryAgent = true;
hasFiltered.clear();
if (!handlersOnly && sendFilteredPointerEvent(event, item)) {
if (event->isAccepted())
return true;
skipDelivery.append(item);
}
// Do not deliverMatchingPointsTo any item for which the filtering parent already intercepted the event,
// nor to any item which already had a chance to filter.
if (skipDelivery.contains(item))
continue;
// sendFilteredPointerEvent() changed the QEventPoint::accepted() state,
// but per-point acceptance is opt-in during normal delivery to items.
for (int i = 0; i < event->pointCount(); ++i)
event->point(i).setAccepted(false);
deliverMatchingPointsToItem(item, false, event, handlersOnly);
if (event->allPointsAccepted())
handlersOnly = true;
}
// Return this because it's true if all events were accepted, rather than
// event->allPointsAccepted(), which can be false even if the event was accepted, because the
// event points' accepted states are set to false before delivery.
return handlersOnly;
}
/*! \internal
Deliver \a pointerEvent to \a item and its handlers, if any.
If \a handlersOnly is \c true, skip QQuickItem::event() and just visit its
handlers via QQuickItemPrivate::handlePointerEvent().
This function exists just to de-duplicate the common code between
deliverPressOrReleaseEvent() and deliverUpdatedPoints().
*/
void QQuickDeliveryAgentPrivate::deliverMatchingPointsToItem(QQuickItem *item, bool isGrabber, QPointerEvent *pointerEvent, bool handlersOnly)
{
QQuickItemPrivate *itemPrivate = QQuickItemPrivate::get(item);
#if defined(Q_OS_ANDROID) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
// QTBUG-85379
// In QT_VERSION below 6.0.0 touchEnabled for QtQuickItems is set by default to true
// It causes delivering touch events to Items which are not interested
// In some cases (like using Material Style in Android) it may cause a crash
if (itemPrivate->wasDeleted)
return;
#endif
localizePointerEvent(pointerEvent, item);
bool isMouse = isMouseEvent(pointerEvent);
// Let the Item's handlers (if any) have the event first.
// However, double click should never be delivered to handlers.
if (pointerEvent->type() != QEvent::MouseButtonDblClick)
itemPrivate->handlePointerEvent(pointerEvent);
if (handlersOnly)
return;
// If all points are released and the item is not the grabber, it doesn't get the event.
// But if at least one point is still pressed, we might be in a potential gesture-takeover scenario.
if (pointerEvent->isEndEvent() && !pointerEvent->isUpdateEvent()
&& !exclusiveGrabbers(pointerEvent).contains(item))
return;
// If any parent filters the event, we're done.
if (sendFilteredPointerEvent(pointerEvent, item))
return;
// TODO: unite this mouse point delivery with the synthetic mouse event below
// TODO: remove isGrabber then?
if (isMouse) {
auto button = static_cast<QSinglePointEvent *>(pointerEvent)->button();
if ((isGrabber && button == Qt::NoButton) || item->acceptedMouseButtons().testFlag(button)) {
// The only reason to already have a mouse grabber here is
// synthetic events - flickable sends one when setPressDelay is used.
auto oldMouseGrabber = pointerEvent->exclusiveGrabber(pointerEvent->point(0));
pointerEvent->accept();
if (isGrabber && sendFilteredPointerEvent(pointerEvent, item))
return;
localizePointerEvent(pointerEvent, item);
QCoreApplication::sendEvent(item, pointerEvent);
if (pointerEvent->isAccepted()) {
auto &point = pointerEvent->point(0);
auto mouseGrabber = pointerEvent->exclusiveGrabber(point);
if (mouseGrabber && mouseGrabber != item && mouseGrabber != oldMouseGrabber) {
// Normally we don't need item->mouseUngrabEvent() here, because QQuickDeliveryAgentPrivate::onGrabChanged does it.
// However, if one item accepted the mouse event, it expects to have the grab and be in "pressed" state,
// because accepting implies grabbing. But before it actually gets the grab, another item could steal it.
// In that case, onGrabChanged() does NOT notify the item that accepted the event that it's not getting the grab after all.
// So after ensuring that it's not redundant, we send a notification here, for that case (QTBUG-55325).
if (item != lastUngrabbed) {
item->mouseUngrabEvent();
lastUngrabbed = item;
}
} else if (item->isEnabled() && item->isVisible() && point.state() == QEventPoint::State::Pressed) {
pointerEvent->setExclusiveGrabber(point, item);
}
point.setAccepted(true);
}
return;
}
}
if (!isTouchEvent(pointerEvent))
return;
bool eventAccepted = false;
QMutableTouchEvent touchEvent;
itemPrivate->localizedTouchEvent(static_cast<QTouchEvent *>(pointerEvent), false, &touchEvent);
if (touchEvent.type() == QEvent::None)
return; // no points inside this item
if (item->acceptTouchEvents()) {
qCDebug(lcTouch) << "considering delivering" << &touchEvent << " to " << item;
// Deliver the touch event to the given item
qCDebug(lcTouch) << "actually delivering" << &touchEvent << " to " << item;
QCoreApplication::sendEvent(item, &touchEvent);
eventAccepted = touchEvent.isAccepted();
} else {
// If the touch event wasn't accepted, synthesize a mouse event and see if the item wants it.
if (Q_LIKELY(QCoreApplication::testAttribute(Qt::AA_SynthesizeMouseForUnhandledTouchEvents)) &&
!eventAccepted && (itemPrivate->acceptedMouseButtons() & Qt::LeftButton))
deliverTouchAsMouse(item, &touchEvent);
return;
}
Q_ASSERT(item->acceptTouchEvents()); // else we would've returned early above
if (eventAccepted) {
bool isPressOrRelease = pointerEvent->isBeginEvent() || pointerEvent->isEndEvent();
for (int i = 0; i < touchEvent.pointCount(); ++i) {
auto &point = touchEvent.point(i);
// legacy-style delivery: if the item doesn't reject the event, that means it handled ALL the points
point.setAccepted();
// but don't let the root of a subscene implicitly steal the grab from some other item (such as one of its children)
if (isPressOrRelease && !(itemPrivate->deliveryAgent() && pointerEvent->exclusiveGrabber(point)))
pointerEvent->setExclusiveGrabber(point, item);
}
} else {
// But if the event was not accepted then we know this item
// will not be interested in further updates for those touchpoint IDs either.
for (const auto &point: touchEvent.points()) {
if (point.state() == QEventPoint::State::Pressed) {
if (pointerEvent->exclusiveGrabber(point) == item) {
qCDebug(lcTouchTarget) << "TP" << Qt::hex << point.id() << "disassociated";
pointerEvent->setExclusiveGrabber(point, nullptr);
}
}
}
}
}
#if QT_CONFIG(quick_draganddrop)
void QQuickDeliveryAgentPrivate::deliverDragEvent(QQuickDragGrabber *grabber, QEvent *event)
{
QObject *formerTarget = grabber->target();
grabber->resetTarget();
QQuickDragGrabber::iterator grabItem = grabber->begin();
if (grabItem != grabber->end()) {
Q_ASSERT(event->type() != QEvent::DragEnter);
if (event->type() == QEvent::Drop) {
QDropEvent *e = static_cast<QDropEvent *>(event);
for (e->setAccepted(false); !e->isAccepted() && grabItem != grabber->end(); grabItem = grabber->release(grabItem)) {
QPointF p = (**grabItem)->mapFromScene(e->position().toPoint());
QDropEvent translatedEvent(
p.toPoint(),
e->possibleActions(),
e->mimeData(),
e->buttons(),
e->modifiers());
QQuickDropEventEx::copyActions(&translatedEvent, *e);
QCoreApplication::sendEvent(**grabItem, &translatedEvent);
e->setAccepted(translatedEvent.isAccepted());
e->setDropAction(translatedEvent.dropAction());
grabber->setTarget(**grabItem);
}
}
if (event->type() != QEvent::DragMove) { // Either an accepted drop or a leave.
QDragLeaveEvent leaveEvent;
for (; grabItem != grabber->end(); grabItem = grabber->release(grabItem))
QCoreApplication::sendEvent(**grabItem, &leaveEvent);
grabber->ignoreList().clear();
return;
} else {
QDragMoveEvent *moveEvent = static_cast<QDragMoveEvent *>(event);
// Used to ensure we don't send DragEnterEvents to current drop targets,
// and to detect which current drop targets we have left
QVarLengthArray<QQuickItem*, 64> currentGrabItems;
for (; grabItem != grabber->end(); grabItem = grabber->release(grabItem))
currentGrabItems.append(**grabItem);
// Look for any other potential drop targets that are higher than the current ones
QDragEnterEvent enterEvent(
moveEvent->position(),
moveEvent->possibleActions(),
moveEvent->mimeData(),
moveEvent->buttons(),
moveEvent->modifiers());
QQuickDropEventEx::copyActions(&enterEvent, *moveEvent);
event->setAccepted(deliverDragEvent(grabber, rootItem, &enterEvent, ¤tGrabItems,
formerTarget));
for (grabItem = grabber->begin(); grabItem != grabber->end(); ++grabItem) {
int i = currentGrabItems.indexOf(**grabItem);
if (i >= 0) {
currentGrabItems.remove(i);
// Still grabbed: send move event
QDragMoveEvent translatedEvent(
(**grabItem)->mapFromScene(moveEvent->position()),
moveEvent->possibleActions(),
moveEvent->mimeData(),
moveEvent->buttons(),
moveEvent->modifiers());
QQuickDropEventEx::copyActions(&translatedEvent, *moveEvent);
QCoreApplication::sendEvent(**grabItem, &translatedEvent);
event->setAccepted(translatedEvent.isAccepted());
QQuickDropEventEx::copyActions(moveEvent, translatedEvent);
}
}
// Anything left in currentGrabItems is no longer a drop target and should be sent a DragLeaveEvent
QDragLeaveEvent leaveEvent;
for (QQuickItem *i : currentGrabItems)
QCoreApplication::sendEvent(i, &leaveEvent);
return;
}
}
if (event->type() == QEvent::DragEnter || event->type() == QEvent::DragMove) {
QDragMoveEvent *e = static_cast<QDragMoveEvent *>(event);
QDragEnterEvent enterEvent(
e->position(),
e->possibleActions(),
e->mimeData(),
e->buttons(),
e->modifiers());
QQuickDropEventEx::copyActions(&enterEvent, *e);
event->setAccepted(deliverDragEvent(grabber, rootItem, &enterEvent));
} else {
grabber->ignoreList().clear();
}
}
bool QQuickDeliveryAgentPrivate::deliverDragEvent(
QQuickDragGrabber *grabber, QQuickItem *item, QDragMoveEvent *event,
QVarLengthArray<QQuickItem *, 64> *currentGrabItems, QObject *formerTarget)
{
QQuickItemPrivate *itemPrivate = QQuickItemPrivate::get(item);
if (!item->isVisible() || !item->isEnabled() || QQuickItemPrivate::get(item)->culled)
return false;
QPointF p = item->mapFromScene(event->position().toPoint());
bool itemContained = item->contains(p);
const int itemIndex = grabber->ignoreList().indexOf(item);
if (!itemContained) {
if (itemIndex >= 0)
grabber->ignoreList().remove(itemIndex);
if (itemPrivate->flags & QQuickItem::ItemClipsChildrenToShape)
return false;
}
QDragEnterEvent enterEvent(
event->position(),
event->possibleActions(),
event->mimeData(),
event->buttons(),
event->modifiers());
QQuickDropEventEx::copyActions(&enterEvent, *event);
QList<QQuickItem *> children = itemPrivate->paintOrderChildItems();
// Check children in front of this item first
for (int ii = children.size() - 1; ii >= 0; --ii) {
if (children.at(ii)->z() < 0)
continue;
if (deliverDragEvent(grabber, children.at(ii), &enterEvent, currentGrabItems, formerTarget))
return true;
}
if (itemContained) {
// If this item is currently grabbed, don't send it another DragEnter,
// just grab it again if it's still contained.
if (currentGrabItems && currentGrabItems->contains(item)) {
grabber->grab(item);
grabber->setTarget(item);
return true;
}
if (event->type() == QEvent::DragMove || itemPrivate->flags & QQuickItem::ItemAcceptsDrops) {
if (event->type() == QEvent::DragEnter) {
if (formerTarget) {
QQuickItem *formerTargetItem = qobject_cast<QQuickItem *>(formerTarget);
if (formerTargetItem && currentGrabItems) {
QDragLeaveEvent leaveEvent;
QCoreApplication::sendEvent(formerTarget, &leaveEvent);
// Remove the item from the currentGrabItems so a leave event won't be generated
// later on
currentGrabItems->removeAll(formerTarget);
}
} else if (itemIndex >= 0) {
return false;
}
}
QDragMoveEvent translatedEvent(p, event->possibleActions(), event->mimeData(),
event->buttons(), event->modifiers(), event->type());
QQuickDropEventEx::copyActions(&translatedEvent, *event);
translatedEvent.setAccepted(event->isAccepted());
QCoreApplication::sendEvent(item, &translatedEvent);
event->setAccepted(translatedEvent.isAccepted());
event->setDropAction(translatedEvent.dropAction());
if (event->type() == QEvent::DragEnter) {
if (translatedEvent.isAccepted()) {
grabber->grab(item);
grabber->setTarget(item);
return true;
} else if (itemIndex < 0) {
grabber->ignoreList().append(item);
}
} else {
return true;
}
}
}
// Check children behind this item if this item or any higher children have not accepted
for (int ii = children.size() - 1; ii >= 0; --ii) {
if (children.at(ii)->z() >= 0)
continue;
if (deliverDragEvent(grabber, children.at(ii), &enterEvent, currentGrabItems, formerTarget))
return true;
}
return false;
}
#endif // quick_draganddrop
/*! \internal
Allow \a filteringParent to filter \a event on behalf of \a receiver, via
QQuickItem::childMouseEventFilter(). This happens right \e before we would
send \a event to \a receiver.
Returns \c true only if \a event has been intercepted (by \a filteringParent
or some other filtering ancestor) and should \e not be sent to \a receiver.
*/
bool QQuickDeliveryAgentPrivate::sendFilteredPointerEvent(QPointerEvent *event, QQuickItem *receiver, QQuickItem *filteringParent)
{
return sendFilteredPointerEventImpl(event, receiver, filteringParent ? filteringParent : receiver->parentItem());
}
/*! \internal
The recursive implementation of sendFilteredPointerEvent().
*/
bool QQuickDeliveryAgentPrivate::sendFilteredPointerEventImpl(QPointerEvent *event, QQuickItem *receiver, QQuickItem *filteringParent)
{
if (!allowChildEventFiltering)
return false;
if (!filteringParent)
return false;
bool filtered = false;
const bool hasHandlers = QQuickItemPrivate::get(receiver)->hasPointerHandlers();
if (filteringParent->filtersChildMouseEvents() && !hasFiltered.contains(filteringParent)) {
hasFiltered.append(filteringParent);
if (isMouseEvent(event)) {
if (receiver->acceptedMouseButtons()) {
const bool wasAccepted = event->allPointsAccepted();
Q_ASSERT(event->pointCount());
localizePointerEvent(event, receiver);
event->setAccepted(true);
auto oldMouseGrabber = event->exclusiveGrabber(event->point(0));
if (filteringParent->childMouseEventFilter(receiver, event)) {
qCDebug(lcMouse) << "mouse event intercepted by childMouseEventFilter of " << filteringParent;
skipDelivery.append(filteringParent);
filtered = true;
if (event->isAccepted() && event->isBeginEvent()) {
auto &point = event->point(0);
auto mouseGrabber = event->exclusiveGrabber(point);
if (mouseGrabber && mouseGrabber != receiver && mouseGrabber != oldMouseGrabber) {
receiver->mouseUngrabEvent();
} else {
event->setExclusiveGrabber(point, receiver);
}
}
} else {
// Restore accepted state if the event was not filtered.
event->setAccepted(wasAccepted);
}
}
} else if (isTouchEvent(event)) {
const bool acceptsTouchEvents = receiver->acceptTouchEvents() || hasHandlers;
auto device = event->device();
if (isTouchPadWithMouseEmulation(event->device())) {
qCDebug(lcTouchTarget) << "skipping filtering of synth-mouse event from" << device;
} else if (acceptsTouchEvents || receiver->acceptedMouseButtons()) {
// get a touch event customized for delivery to filteringParent
// TODO should not be necessary? because QQuickDeliveryAgentPrivate::deliverMatchingPointsToItem() does it
QMutableTouchEvent filteringParentTouchEvent;
QQuickItemPrivate::get(receiver)->localizedTouchEvent(static_cast<QTouchEvent *>(event), true, &filteringParentTouchEvent);
if (filteringParentTouchEvent.type() != QEvent::None) {
qCDebug(lcTouch) << "letting parent" << filteringParent << "filter for" << receiver << &filteringParentTouchEvent;
filtered = filteringParent->childMouseEventFilter(receiver, &filteringParentTouchEvent);
if (filtered) {
qCDebug(lcTouch) << "touch event intercepted by childMouseEventFilter of " << filteringParent;
event->setAccepted(filteringParentTouchEvent.isAccepted());
skipDelivery.append(filteringParent);
if (event->isAccepted()) {
for (auto point : filteringParentTouchEvent.points()) {
const QQuickItem *exclusiveGrabber = qobject_cast<const QQuickItem *>(event->exclusiveGrabber(point));
// Transfer the grab to the filtering parent unless the current exclusive grabber has
// keepTouchGrab set AND the filtering parent is not an ancestor of that grabber.
// If filteringParent is an ancestor of exclusiveGrabber (e.g. PinchArea wrapping a Flickable),
// allow the transfer: presumably the user intended the parent to intercept multi-touch gestures.
// But only if the grabber itself accepts touch events — meaning it set keepTouchGrab
// on its own behalf (like Flickable). If the grabber doesn't accept touch events
// (like a passive QQuickText), keepTouchGrab was set by the filtering parent's
// childMouseEventFilter to maintain the filter-based event routing pattern
// (e.g. SplitView sets a handle child as grabber to keep receiving filter calls).
const bool grabberInsideFilteringParent = exclusiveGrabber &&
exclusiveGrabber->acceptTouchEvents() &&
filteringParent->isAncestorOf(const_cast<QQuickItem *>(exclusiveGrabber));
if (!exclusiveGrabber || !exclusiveGrabber->keepTouchGrab() || grabberInsideFilteringParent)
event->setExclusiveGrabber(point, filteringParent);
}
// QPointerEvent::setAccepted(true) marks all individual QEventPoints as accepted,
// which causes localizedTouchEvent() to skip them (line 9577: if (p.isAccepted()) continue).
// This would prevent any ancestor filtering parent (e.g. PinchArea wrapping this Flickable)
// from seeing the points in the recursive sendFilteredPointerEventImpl() call below.
// Reset per-point accepted state so ancestor filters can still see all relevant points.
// The overall event->isAccepted() remains true to stop non-filter item delivery.
for (int i = 0; i < event->pointCount(); ++i)
event->point(i).setAccepted(false);
}
} else if (Q_LIKELY(QCoreApplication::testAttribute(Qt::AA_SynthesizeMouseForUnhandledTouchEvents)) &&
!filteringParent->acceptTouchEvents()) {
qCDebug(lcTouch) << "touch event NOT intercepted by childMouseEventFilter of " << filteringParent
<< "; accepts touch?" << filteringParent->acceptTouchEvents()
<< "receiver accepts touch?" << acceptsTouchEvents
<< "so, letting parent filter a synth-mouse event";
// filteringParent didn't filter the touch event. Give it a chance to filter a synthetic mouse event.
for (auto &tp : filteringParentTouchEvent.points()) {
QEvent::Type t;
switch (tp.state()) {
case QEventPoint::State::Pressed:
t = QEvent::MouseButtonPress;
break;
case QEventPoint::State::Released:
t = QEvent::MouseButtonRelease;
break;
case QEventPoint::State::Stationary:
continue;
default:
t = QEvent::MouseMove;
break;
}
bool touchMouseUnset = (touchMouseId == -1);
// Only deliver mouse event if it is the touchMouseId or it could become the touchMouseId
if (touchMouseUnset || touchMouseId == tp.id()) {
// convert filteringParentTouchEvent (which is already transformed wrt local position, velocity, etc.)
// into a synthetic mouse event, and let childMouseEventFilter() have another chance with that
QMutableSinglePointEvent mouseEvent;
touchToMouseEvent(t, tp, &filteringParentTouchEvent, &mouseEvent);
// If a filtering item calls QQuickWindow::mouseGrabberItem(), it should
// report the touchpoint's grabber. Whenever we send a synthetic mouse event,
// touchMouseId and touchMouseDevice must be set, even if it's only temporarily and isn't grabbed.
touchMouseId = tp.id();
touchMouseDevice = event->pointingDevice();
filtered = filteringParent->childMouseEventFilter(receiver, &mouseEvent);
if (filtered) {
qCDebug(lcTouch) << "touch event intercepted as synth mouse event by childMouseEventFilter of " << filteringParent;
event->setAccepted(mouseEvent.isAccepted());
skipDelivery.append(filteringParent);
if (event->isAccepted() && event->isBeginEvent()) {
qCDebug(lcTouchTarget) << "TP (mouse)" << Qt::hex << tp.id() << "->" << filteringParent;
filteringParentTouchEvent.setExclusiveGrabber(tp, filteringParent);
touchMouseUnset = false; // We want to leave touchMouseId and touchMouseDevice set
filteringParent->grabMouse();
}
}
if (touchMouseUnset)
// Now that we're done sending a synth mouse event, and it wasn't grabbed,
// the touchpoint is no longer acting as a synthetic mouse. Restore previous state.
cancelTouchMouseSynthesis();
mouseEvent.point(0).setAccepted(false); // because touchToMouseEvent() set it true
// Only one touchpoint can be treated as a synthetic mouse, so after childMouseEventFilter
// has been called once, we're done with this loop over the touchpoints.
break;
}
}
}
}
}
}
}
return sendFilteredPointerEventImpl(event, receiver, filteringParent->parentItem()) || filtered;
}
/*! \internal
Allow \a filteringParent to filter \a event on behalf of \a receiver, via
QQuickItem::childMouseEventFilter(). This happens right \e before we would
send \a event to \a receiver.
Returns \c true only if \a event has been intercepted (by \a filteringParent
or some other filtering ancestor) and should \e not be sent to \a receiver.
Unlike sendFilteredPointerEvent(), this version does not synthesize a
mouse event from touch (presumably it's already an actual mouse event).
*/
bool QQuickDeliveryAgentPrivate::sendFilteredMouseEvent(QEvent *event, QQuickItem *receiver, QQuickItem *filteringParent)
{
if (!filteringParent)
return false;
QQuickItemPrivate *filteringParentPrivate = QQuickItemPrivate::get(filteringParent);
if (filteringParentPrivate->replayingPressEvent)
return false;
bool filtered = false;
if (filteringParentPrivate->filtersChildMouseEvents && !hasFiltered.contains(filteringParent)) {
hasFiltered.append(filteringParent);
if (filteringParent->childMouseEventFilter(receiver, event)) {
filtered = true;
skipDelivery.append(filteringParent);
}
qCDebug(lcMouseTarget) << "for" << receiver << filteringParent << "childMouseEventFilter ->" << filtered;
}
return sendFilteredMouseEvent(event, receiver, filteringParent->parentItem()) || filtered;
}
/*! \internal
Returns \c true if the movement delta \a d in pixels along the \a axis
exceeds \a startDragThreshold if it is set, or QStyleHints::startDragDistance();
\e or, if QEventPoint::velocity() of \a event exceeds QStyleHints::startDragVelocity().
\sa QQuickPointerHandlerPrivate::dragOverThreshold()
*/
bool QQuickDeliveryAgentPrivate::dragOverThreshold(qreal d, Qt::Axis axis, QMouseEvent *event, int startDragThreshold)
{
QStyleHints *styleHints = QGuiApplication::styleHints();
bool dragVelocityLimitAvailable = event->device()->capabilities().testFlag(QInputDevice::Capability::Velocity)
&& styleHints->startDragVelocity();
bool overThreshold = qAbs(d) > (startDragThreshold >= 0 ? startDragThreshold : styleHints->startDragDistance());
if (dragVelocityLimitAvailable) {
QVector2D velocityVec = event->point(0).velocity();
qreal velocity = axis == Qt::XAxis ? velocityVec.x() : velocityVec.y();
overThreshold |= qAbs(velocity) > styleHints->startDragVelocity();
}
return overThreshold;
}
/*! \internal
Returns \c true if the movement delta \a d in pixels along the \a axis
exceeds \a startDragThreshold if it is set, or QStyleHints::startDragDistance();
\e or, if QEventPoint::velocity() of \a tp exceeds QStyleHints::startDragVelocity().
\sa QQuickPointerHandlerPrivate::dragOverThreshold()
*/
bool QQuickDeliveryAgentPrivate::dragOverThreshold(qreal d, Qt::Axis axis, const QEventPoint &tp, int startDragThreshold)
{
QStyleHints *styleHints = qApp->styleHints();
bool overThreshold = qAbs(d) > (startDragThreshold >= 0 ? startDragThreshold : styleHints->startDragDistance());
const bool dragVelocityLimitAvailable = (styleHints->startDragVelocity() > 0);
if (!overThreshold && dragVelocityLimitAvailable) {
qreal velocity = axis == Qt::XAxis ? tp.velocity().x() : tp.velocity().y();
overThreshold |= qAbs(velocity) > styleHints->startDragVelocity();
}
return overThreshold;
}
/*! \internal
Returns \c true if the movement \a delta in pixels exceeds QStyleHints::startDragDistance().
\sa QQuickDeliveryAgentPrivate::dragOverThreshold()
*/
bool QQuickDeliveryAgentPrivate::dragOverThreshold(QVector2D delta)
{
int threshold = qApp->styleHints()->startDragDistance();
return qAbs(delta.x()) > threshold || qAbs(delta.y()) > threshold;
}
/*!
\internal
Returns all items that could potentially want \a event.
(Similar to \l pointerTargets(), necessary because QContextMenuEvent is not
a QPointerEvent.)
*/
QList<QQuickItem *> QQuickDeliveryAgentPrivate::contextMenuTargets(QQuickItem *item, const QContextMenuEvent *event) const
{
auto predicate = [](QQuickItem *, const QEvent *) -> std::optional<bool> {
return std::nullopt;
};
const auto pos = event->pos().isNull() ? activeFocusItem->mapToScene({}).toPoint() : event->pos();
if (event->pos().isNull())
qCDebug(lcContextMenu) << "for QContextMenuEvent, active focus item is" << activeFocusItem << "@" << pos;
return eventTargets(item, event, -1, pos, pos, predicate);
}
/*!
\internal
Based on \l deliverPointerEvent().
*/
void QQuickDeliveryAgentPrivate::deliverContextMenuEvent(QContextMenuEvent *event)
{
skipDelivery.clear();
QList<QQuickItem *> targetItems = contextMenuTargets(rootItem, event);
qCDebug(lcContextMenu) << "delivering context menu event" << event << "to" << targetItems.size() << "target item(s)";
QList<QPointer<QQuickItem>> safeTargetItems(targetItems.begin(), targetItems.end());
for (auto &item : safeTargetItems) {
qCDebug(lcContextMenu) << "- attempting to deliver to" << item;
if (item.isNull())
continue;
// failsafe: when items get into a subscene somehow, ensure that QQuickItemPrivate::deliveryAgent() can find it
if (isSubsceneAgent)
QQuickItemPrivate::get(item)->maybeHasSubsceneDeliveryAgent = true;
QCoreApplication::sendEvent(item, event);
if (event->isAccepted())
return;
}
}
#ifndef QT_NO_DEBUG_STREAM
QDebug operator<<(QDebug debug, const QQuickDeliveryAgent *da)
{
QDebugStateSaver saver(debug);
debug.nospace();
if (!da) {
debug << "QQuickDeliveryAgent(0)";
return debug;
}
debug << "QQuickDeliveryAgent(";
if (!da->objectName().isEmpty())
debug << da->objectName() << ' ';
auto root = da->rootItem();
if (Q_LIKELY(root)) {
debug << "root=" << root->metaObject()->className();
if (!root->objectName().isEmpty())
debug << ' ' << root->objectName();
} else {
debug << "root=0";
}
debug << ')';
return debug;
}
#endif
QT_END_NAMESPACE
#include "moc_qquickdeliveryagent_p.cpp"