blob: 22b04eabd8fd219c683de8dea17de7a4c5692ef4 [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the test suite of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include <qtest.h>
#include <QDebug>
#include <QMimeData>
#include <QTouchEvent>
#include <QtQuick/QQuickItem>
#include <QtQuick/QQuickView>
#include <QtQuick/QQuickWindow>
#include <QtQml/QQmlEngine>
#include <QtQml/QQmlComponent>
#include <QtQuick/private/qquickrectangle_p.h>
#include <QtQuick/private/qquickloader_p.h>
#include <QtQuick/private/qquickmousearea_p.h>
#include "../../shared/util.h"
#include "../shared/visualtestutil.h"
#include "../shared/viewtestutil.h"
#include <QSignalSpy>
#include <private/qquickwindow_p.h>
#include <private/qguiapplication_p.h>
#include <QRunnable>
#include <QOpenGLFunctions>
#include <QSGRendererInterface>
Q_LOGGING_CATEGORY(lcTests, "qt.quick.tests")
struct TouchEventData {
QEvent::Type type;
QWidget *widget;
QWindow *window;
Qt::TouchPointStates states;
QList<QTouchEvent::TouchPoint> touchPoints;
};
static QTouchEvent::TouchPoint makeTouchPoint(QQuickItem *item, const QPointF &p, const QPointF &lastPoint = QPointF())
{
QPointF last = lastPoint.isNull() ? p : lastPoint;
QTouchEvent::TouchPoint tp;
tp.setPos(p);
tp.setLastPos(last);
tp.setScenePos(item->mapToScene(p));
tp.setLastScenePos(item->mapToScene(last));
tp.setScreenPos(item->window()->mapToGlobal(tp.scenePos().toPoint()));
tp.setLastScreenPos(item->window()->mapToGlobal(tp.lastScenePos().toPoint()));
return tp;
}
static TouchEventData makeTouchData(QEvent::Type type, QWindow *w, Qt::TouchPointStates states = {},
const QList<QTouchEvent::TouchPoint>& touchPoints = QList<QTouchEvent::TouchPoint>())
{
TouchEventData d = { type, nullptr, w, states, touchPoints };
return d;
}
static TouchEventData makeTouchData(QEvent::Type type, QWindow *w, Qt::TouchPointStates states, const QTouchEvent::TouchPoint &touchPoint)
{
QList<QTouchEvent::TouchPoint> points;
points << touchPoint;
return makeTouchData(type, w, states, points);
}
#define COMPARE_TOUCH_POINTS(tp1, tp2) \
{ \
QCOMPARE(tp1.pos(), tp2.pos()); \
QCOMPARE(tp1.lastPos(), tp2.lastPos()); \
QCOMPARE(tp1.scenePos(), tp2.scenePos()); \
QCOMPARE(tp1.lastScenePos(), tp2.lastScenePos()); \
QCOMPARE(tp1.screenPos(), tp2.screenPos()); \
QCOMPARE(tp1.lastScreenPos(), tp2.lastScreenPos()); \
}
#define COMPARE_TOUCH_DATA(d1, d2) \
{ \
QCOMPARE((int)d1.type, (int)d2.type); \
QCOMPARE(d1.widget, d2.widget); \
QCOMPARE((int)d1.states, (int)d2.states); \
QCOMPARE(d1.touchPoints.count(), d2.touchPoints.count()); \
for (int i=0; i<d1.touchPoints.count(); i++) { \
COMPARE_TOUCH_POINTS(d1.touchPoints[i], d2.touchPoints[i]); \
} \
}
class RootItemAccessor : public QQuickItem
{
Q_OBJECT
public:
RootItemAccessor()
: m_rootItemDestroyed(false)
, m_rootItem(nullptr)
{
}
Q_INVOKABLE QQuickItem *contentItem()
{
if (!m_rootItem) {
QQuickWindowPrivate *c = QQuickWindowPrivate::get(window());
m_rootItem = c->contentItem;
QObject::connect(m_rootItem, SIGNAL(destroyed()), this, SLOT(rootItemDestroyed()));
}
return m_rootItem;
}
bool isRootItemDestroyed() {return m_rootItemDestroyed;}
public slots:
void rootItemDestroyed() {
m_rootItemDestroyed = true;
}
private:
bool m_rootItemDestroyed;
QQuickItem *m_rootItem;
};
class TestTouchItem : public QQuickRectangle
{
Q_OBJECT
public:
TestTouchItem(QQuickItem *parent = nullptr)
: QQuickRectangle(parent), acceptTouchEvents(true), acceptMouseEvents(true),
mousePressCount(0), mouseMoveCount(0),
spinLoopWhenPressed(false), touchEventCount(0),
mouseUngrabEventCount(0)
{
border()->setWidth(1);
setAcceptedMouseButtons(Qt::LeftButton);
setFiltersChildMouseEvents(true);
}
void reset() {
acceptTouchEvents = acceptMouseEvents = true;
setEnabled(true);
setVisible(true);
lastEvent = makeTouchData(QEvent::None, window(), {}, QList<QTouchEvent::TouchPoint>());//CHECK_VALID
lastVelocity = lastVelocityFromMouseMove = QVector2D();
lastMousePos = QPointF();
lastMouseCapabilityFlags = 0;
touchEventCount = 0;
mouseMoveCount = 0;
mouseUngrabEventCount = 0;
}
static void clearMouseEventCounters()
{
mousePressNum = mouseMoveNum = mouseReleaseNum = 0;
}
void clearTouchEventCounter()
{
touchEventCount = 0;
}
bool acceptTouchEvents;
bool acceptMouseEvents;
bool grabOnRelease = false;
TouchEventData lastEvent;
int mousePressCount;
int mouseMoveCount;
bool spinLoopWhenPressed;
int touchEventCount;
int mouseUngrabEventCount;
QVector2D lastVelocity;
QVector2D lastVelocityFromMouseMove;
QPointF lastMousePos;
int lastMouseCapabilityFlags;
void touchEvent(QTouchEvent *event) {
if (!acceptTouchEvents) {
event->ignore();
return;
}
++touchEventCount;
lastEvent = makeTouchData(event->type(), event->window(), event->touchPointStates(), event->touchPoints());
if (event->device()->capabilities().testFlag(QTouchDevice::Velocity) && !event->touchPoints().isEmpty()) {
lastVelocity = event->touchPoints().first().velocity();
} else {
lastVelocity = QVector2D();
}
if (spinLoopWhenPressed && event->touchPointStates().testFlag(Qt::TouchPointPressed)) {
QCoreApplication::processEvents();
}
}
void mousePressEvent(QMouseEvent *e) {
if (!acceptMouseEvents) {
e->ignore();
return;
}
mousePressCount = ++mousePressNum;
lastMousePos = e->pos();
lastMouseCapabilityFlags = QGuiApplicationPrivate::mouseEventCaps(e);
}
void mouseMoveEvent(QMouseEvent *e) {
if (!acceptMouseEvents) {
e->ignore();
return;
}
mouseMoveCount = ++mouseMoveNum;
lastVelocityFromMouseMove = QGuiApplicationPrivate::mouseEventVelocity(e);
lastMouseCapabilityFlags = QGuiApplicationPrivate::mouseEventCaps(e);
lastMousePos = e->pos();
}
void mouseReleaseEvent(QMouseEvent *e) {
if (!acceptMouseEvents) {
e->ignore();
return;
}
++mouseReleaseNum;
lastMousePos = e->pos();
lastMouseCapabilityFlags = QGuiApplicationPrivate::mouseEventCaps(e);
}
void mouseUngrabEvent() {
++mouseUngrabEventCount;
}
bool childMouseEventFilter(QQuickItem *item, QEvent *e) {
qCDebug(lcTests) << objectName() << "filtering" << e << "ahead of delivery to" << item->metaObject()->className() << item->objectName();
switch (e->type()) {
case QEvent::MouseButtonPress:
mousePressCount = ++mousePressNum;
break;
case QEvent::MouseButtonRelease:
if (grabOnRelease)
grabMouse();
break;
case QEvent::MouseMove:
mouseMoveCount = ++mouseMoveNum;
break;
default:
break;
}
return false;
}
static int mousePressNum, mouseMoveNum, mouseReleaseNum;
};
int TestTouchItem::mousePressNum = 0;
int TestTouchItem::mouseMoveNum = 0;
int TestTouchItem::mouseReleaseNum = 0;
class EventFilter : public QObject
{
public:
bool eventFilter(QObject *watched, QEvent *event) {
Q_UNUSED(watched);
events.append(event->type());
return false;
}
QList<int> events;
};
class ConstantUpdateItem : public QQuickItem
{
Q_OBJECT
public:
ConstantUpdateItem(QQuickItem *parent = nullptr) : QQuickItem(parent), iterations(0) {setFlag(ItemHasContents);}
int iterations;
protected:
QSGNode* updatePaintNode(QSGNode *, UpdatePaintNodeData *){
iterations++;
update();
return nullptr;
}
};
class MouseRecordingWindow : public QQuickWindow
{
public:
explicit MouseRecordingWindow(QWindow *parent = nullptr) : QQuickWindow(parent) { }
protected:
void mousePressEvent(QMouseEvent *event) override {
qCDebug(lcTests) << event;
m_mouseEvents << *event;
QQuickWindow::mousePressEvent(event);
}
void mouseMoveEvent(QMouseEvent *event) override {
qCDebug(lcTests) << event;
m_mouseEvents << *event;
QQuickWindow::mouseMoveEvent(event);
}
void mouseReleaseEvent(QMouseEvent *event) override {
qCDebug(lcTests) << event;
m_mouseEvents << *event;
QQuickWindow::mouseReleaseEvent(event);
}
public:
QList<QMouseEvent> m_mouseEvents;
};
class MouseRecordingItem : public QQuickItem
{
public:
MouseRecordingItem(bool acceptTouch, QQuickItem *parent = nullptr)
: QQuickItem(parent)
, m_acceptTouch(acceptTouch)
{
setSize(QSizeF(300, 300));
setAcceptedMouseButtons(Qt::LeftButton);
}
protected:
void touchEvent(QTouchEvent* event) override {
event->setAccepted(m_acceptTouch);
m_touchEvents << *event;
qCDebug(lcTests) << "accepted?" << event->isAccepted() << event;
}
void mousePressEvent(QMouseEvent *event) override {
qCDebug(lcTests) << event;
m_mouseEvents << *event;
}
void mouseMoveEvent(QMouseEvent *event) override {
qCDebug(lcTests) << event;
m_mouseEvents << *event;
}
void mouseReleaseEvent(QMouseEvent *event) override {
qCDebug(lcTests) << event;
m_mouseEvents << *event;
}
void mouseDoubleClickEvent(QMouseEvent *event) override {
qCDebug(lcTests) << event;
m_mouseEvents << *event;
}
public:
QList<QMouseEvent> m_mouseEvents;
QList<QTouchEvent> m_touchEvents;
private:
bool m_acceptTouch;
};
class tst_qquickwindow : public QQmlDataTest
{
Q_OBJECT
public:
tst_qquickwindow()
: touchDevice(QTest::createTouchDevice())
, touchDeviceWithVelocity(QTest::createTouchDevice())
{
QQuickWindow::setDefaultAlphaBuffer(true);
touchDeviceWithVelocity->setCapabilities(QTouchDevice::Position | QTouchDevice::Velocity);
}
private slots:
void cleanup();
#if QT_CONFIG(opengl)
void openglContextCreatedSignal();
#endif
void aboutToStopSignal();
void constantUpdates();
void constantUpdatesOnWindow_data();
void constantUpdatesOnWindow();
void mouseFiltering();
void headless();
void noUpdateWhenNothingChanges();
void touchEvent_basic();
void touchEvent_propagation();
void touchEvent_propagation_data();
void touchEvent_cancel();
void touchEvent_cancelClearsMouseGrab();
void touchEvent_reentrant();
void touchEvent_velocity();
void mergeTouchPointLists_data();
void mergeTouchPointLists();
void mouseFromTouch_basic();
void synthMouseFromTouch_data();
void synthMouseFromTouch();
void synthMouseDoubleClickFromTouch_data();
void synthMouseDoubleClickFromTouch();
void clearWindow();
void qmlCreation();
void qmlCreationWithScreen();
void clearColor();
void defaultState();
void grab_data();
void grab();
void multipleWindows();
void animationsWhileHidden();
void focusObject();
void focusReason();
void ignoreUnhandledMouseEvents();
void ownershipRootItem();
void hideThenDelete_data();
void hideThenDelete();
void showHideAnimate();
void testExpose();
void requestActivate();
void testWindowVisibilityOrder();
void blockClosing();
void blockCloseMethod();
void crashWhenHoverItemDeleted();
void unloadSubWindow();
void changeVisibilityInCompleted();
void qobjectEventFilter_touch();
void qobjectEventFilter_key();
void qobjectEventFilter_mouse();
#if QT_CONFIG(cursor)
void cursor();
#endif
void animatingSignal();
void contentItemSize();
void defaultSurfaceFormat();
void attachedProperty();
void testRenderJob();
void testHoverChildMouseEventFilter();
void testHoverTimestamp();
void test_circleMapItem();
void pointerEventTypeAndPointCount();
void grabContentItemToImage();
void testDragEventPropertyPropagation();
void findChild();
void testChildMouseEventFilter();
void testChildMouseEventFilter_data();
void cleanupGrabsOnRelease();
#if QT_CONFIG(shortcut)
void testShortCut();
#endif
private:
QTouchDevice *touchDevice;
QTouchDevice *touchDeviceWithVelocity;
};
#if QT_CONFIG(opengl)
Q_DECLARE_METATYPE(QOpenGLContext *);
#endif
void tst_qquickwindow::cleanup()
{
QVERIFY(QGuiApplication::topLevelWindows().isEmpty());
}
#if QT_CONFIG(opengl)
void tst_qquickwindow::openglContextCreatedSignal()
{
qRegisterMetaType<QOpenGLContext *>();
QQuickWindow window;
QSignalSpy spy(&window, SIGNAL(openglContextCreated(QOpenGLContext*)));
window.setTitle(QTest::currentTestFunction());
window.show();
QVERIFY(QTest::qWaitForWindowExposed(&window));
if (window.rendererInterface()->graphicsApi() != QSGRendererInterface::OpenGL)
QSKIP("Skipping OpenGL context test due to not running with OpenGL");
QTRY_VERIFY(spy.size() > 0);
QVariant ctx = spy.at(0).at(0);
QCOMPARE(qvariant_cast<QOpenGLContext *>(ctx), window.openglContext());
}
#endif
void tst_qquickwindow::aboutToStopSignal()
{
QQuickWindow window;
window.setTitle(QTest::currentTestFunction());
window.show();
QVERIFY(QTest::qWaitForWindowExposed(&window));
QSignalSpy spy(&window, SIGNAL(sceneGraphAboutToStop()));
window.hide();
QTRY_VERIFY(spy.count() > 0);
}
//If the item calls update inside updatePaintNode, it should schedule another sync pass
void tst_qquickwindow::constantUpdates()
{
QQuickWindow window;
window.resize(250, 250);
ConstantUpdateItem item(window.contentItem());
window.setTitle(QTest::currentTestFunction());
window.show();
QSignalSpy beforeSpy(&window, SIGNAL(beforeSynchronizing()));
QSignalSpy afterSpy(&window, SIGNAL(afterSynchronizing()));
QTRY_VERIFY(item.iterations > 10);
QTRY_VERIFY(beforeSpy.count() > 10);
QTRY_VERIFY(afterSpy.count() > 10);
}
void tst_qquickwindow::constantUpdatesOnWindow_data()
{
QTest::addColumn<bool>("blockedGui");
QTest::addColumn<QByteArray>("signal");
QQuickWindow window;
window.setTitle(QTest::currentTestFunction());
window.setGeometry(100, 100, 300, 200);
window.show();
QVERIFY(QTest::qWaitForWindowExposed(&window));
const bool threaded = QQuickWindowPrivate::get(&window)->context->thread() != QGuiApplication::instance()->thread();
if (threaded) {
QTest::newRow("blocked, beforeRender") << true << QByteArray(SIGNAL(beforeRendering()));
QTest::newRow("blocked, afterRender") << true << QByteArray(SIGNAL(afterRendering()));
QTest::newRow("blocked, swapped") << true << QByteArray(SIGNAL(frameSwapped()));
}
QTest::newRow("unblocked, beforeRender") << false << QByteArray(SIGNAL(beforeRendering()));
QTest::newRow("unblocked, afterRender") << false << QByteArray(SIGNAL(afterRendering()));
QTest::newRow("unblocked, swapped") << false << QByteArray(SIGNAL(frameSwapped()));
}
class FrameCounter : public QObject
{
Q_OBJECT
public slots:
void incr() { QMutexLocker locker(&m_mutex); ++m_counter; }
public:
FrameCounter() : m_counter(0) {}
int count() { QMutexLocker locker(&m_mutex); int x = m_counter; return x; }
private:
int m_counter;
QMutex m_mutex;
};
void tst_qquickwindow::constantUpdatesOnWindow()
{
QFETCH(bool, blockedGui);
QFETCH(QByteArray, signal);
QQuickWindow window;
window.setTitle(QTest::currentTestFunction());
window.setGeometry(100, 100, 300, 200);
bool ok = connect(&window, signal.constData(), &window, SLOT(update()), Qt::DirectConnection);
Q_ASSERT(ok);
window.show();
QVERIFY(QTest::qWaitForWindowExposed(&window));
FrameCounter counter;
connect(&window, SIGNAL(frameSwapped()), &counter, SLOT(incr()), Qt::DirectConnection);
int frameCount = 10;
QElapsedTimer timer;
timer.start();
if (blockedGui) {
while (counter.count() < frameCount)
QTest::qSleep(100);
QVERIFY(counter.count() >= frameCount);
} else {
window.update();
QTRY_VERIFY(counter.count() > frameCount);
}
window.hide();
}
void tst_qquickwindow::touchEvent_basic()
{
TestTouchItem::clearMouseEventCounters();
QQuickWindow *window = new QQuickWindow;
QScopedPointer<QQuickWindow> cleanup(window);
window->setTitle(QTest::currentTestFunction());
window->resize(250, 250);
window->setPosition(100, 100);
window->show();
QVERIFY(QTest::qWaitForWindowActive(window));
TestTouchItem *bottomItem = new TestTouchItem(window->contentItem());
bottomItem->setObjectName("Bottom Item");
bottomItem->setSize(QSizeF(150, 150));
TestTouchItem *middleItem = new TestTouchItem(bottomItem);
middleItem->setObjectName("Middle Item");
middleItem->setPosition(QPointF(50, 50));
middleItem->setSize(QSizeF(150, 150));
TestTouchItem *topItem = new TestTouchItem(middleItem);
topItem->setObjectName("Top Item");
topItem->setPosition(QPointF(50, 50));
topItem->setSize(QSizeF(150, 150));
QPointF pos(10, 10);
QTest::QTouchEventSequence touchSeq = QTest::touchEvent(window, touchDevice, false);
// press single point
touchSeq.press(0, topItem->mapToScene(pos).toPoint(),window).commit();
QQuickTouchUtils::flush(window);
QTRY_COMPARE(topItem->lastEvent.touchPoints.count(), 1);
QVERIFY(middleItem->lastEvent.touchPoints.isEmpty());
QVERIFY(bottomItem->lastEvent.touchPoints.isEmpty());
// At one point this was failing with kwin (KDE window manager) because window->setPosition(100, 100)
// would put the decorated window at that position rather than the window itself.
COMPARE_TOUCH_DATA(topItem->lastEvent, makeTouchData(QEvent::TouchBegin, window, Qt::TouchPointPressed, makeTouchPoint(topItem, pos)));
topItem->reset();
touchSeq.release(0, topItem->mapToScene(pos).toPoint(), window).commit();
// press multiple points
touchSeq.press(0, topItem->mapToScene(pos).toPoint(), window)
.press(1, bottomItem->mapToScene(pos).toPoint(), window).commit();
QQuickTouchUtils::flush(window);
QCOMPARE(topItem->lastEvent.touchPoints.count(), 1);
QVERIFY(middleItem->lastEvent.touchPoints.isEmpty());
QCOMPARE(bottomItem->lastEvent.touchPoints.count(), 1);
COMPARE_TOUCH_DATA(topItem->lastEvent, makeTouchData(QEvent::TouchBegin, window, Qt::TouchPointPressed, makeTouchPoint(topItem, pos)));
COMPARE_TOUCH_DATA(bottomItem->lastEvent, makeTouchData(QEvent::TouchBegin, window, Qt::TouchPointPressed, makeTouchPoint(bottomItem, pos)));
topItem->reset();
bottomItem->reset();
touchSeq.release(0, topItem->mapToScene(pos).toPoint(), window).release(1, bottomItem->mapToScene(pos).toPoint(), window).commit();
// touch point on top item moves to bottom item, but top item should still receive the event
touchSeq.press(0, topItem->mapToScene(pos).toPoint(), window).commit();
QQuickTouchUtils::flush(window);
touchSeq.move(0, bottomItem->mapToScene(pos).toPoint(), window).commit();
QQuickTouchUtils::flush(window);
QCOMPARE(topItem->lastEvent.touchPoints.count(), 1);
COMPARE_TOUCH_DATA(topItem->lastEvent, makeTouchData(QEvent::TouchUpdate, window, Qt::TouchPointMoved,
makeTouchPoint(topItem, topItem->mapFromItem(bottomItem, pos), pos)));
topItem->reset();
touchSeq.release(0, bottomItem->mapToScene(pos).toPoint(), window).commit();
// touch point on bottom item moves to top item, but bottom item should still receive the event
touchSeq.press(0, bottomItem->mapToScene(pos).toPoint(), window).commit();
QQuickTouchUtils::flush(window);
touchSeq.move(0, topItem->mapToScene(pos).toPoint(), window).commit();
QQuickTouchUtils::flush(window);
QCOMPARE(bottomItem->lastEvent.touchPoints.count(), 1);
COMPARE_TOUCH_DATA(bottomItem->lastEvent, makeTouchData(QEvent::TouchUpdate, window, Qt::TouchPointMoved,
makeTouchPoint(bottomItem, bottomItem->mapFromItem(topItem, pos), pos)));
bottomItem->reset();
touchSeq.release(0, bottomItem->mapToScene(pos).toPoint(), window).commit();
// a single stationary press on an item shouldn't cause an event
touchSeq.press(0, topItem->mapToScene(pos).toPoint(), window).commit();
QQuickTouchUtils::flush(window);
touchSeq.stationary(0)
.press(1, bottomItem->mapToScene(pos).toPoint(), window).commit();
QQuickTouchUtils::flush(window);
QCOMPARE(topItem->lastEvent.touchPoints.count(), 1); // received press only, not stationary
QVERIFY(middleItem->lastEvent.touchPoints.isEmpty());
QCOMPARE(bottomItem->lastEvent.touchPoints.count(), 1);
COMPARE_TOUCH_DATA(topItem->lastEvent, makeTouchData(QEvent::TouchBegin, window, Qt::TouchPointPressed, makeTouchPoint(topItem, pos)));
COMPARE_TOUCH_DATA(bottomItem->lastEvent, makeTouchData(QEvent::TouchBegin, window, Qt::TouchPointPressed, makeTouchPoint(bottomItem, pos)));
topItem->reset();
bottomItem->reset();
// cleanup: what is pressed must be released
// Otherwise you will get an assertion failure:
// ASSERT: "itemForTouchPointId.isEmpty()" in file items/qquickwindow.cpp
touchSeq.release(0, pos.toPoint(), window).release(1, pos.toPoint(), window).commit();
QQuickTouchUtils::flush(window);
// move touch point from top item to bottom, and release
touchSeq.press(0, topItem->mapToScene(pos).toPoint(),window).commit();
QQuickTouchUtils::flush(window);
touchSeq.release(0, bottomItem->mapToScene(pos).toPoint(),window).commit();
QQuickTouchUtils::flush(window);
QCOMPARE(topItem->lastEvent.touchPoints.count(), 1);
COMPARE_TOUCH_DATA(topItem->lastEvent, makeTouchData(QEvent::TouchEnd, window, Qt::TouchPointReleased,
makeTouchPoint(topItem, topItem->mapFromItem(bottomItem, pos), pos)));
topItem->reset();
// release while another point is pressed
touchSeq.press(0, topItem->mapToScene(pos).toPoint(),window)
.press(1, bottomItem->mapToScene(pos).toPoint(), window).commit();
QQuickTouchUtils::flush(window);
touchSeq.move(0, bottomItem->mapToScene(pos).toPoint(), window).commit();
QQuickTouchUtils::flush(window);
touchSeq.release(0, bottomItem->mapToScene(pos).toPoint(), window)
.stationary(1).commit();
QQuickTouchUtils::flush(window);
QCOMPARE(topItem->lastEvent.touchPoints.count(), 1);
QVERIFY(middleItem->lastEvent.touchPoints.isEmpty());
QCOMPARE(bottomItem->lastEvent.touchPoints.count(), 1);
COMPARE_TOUCH_DATA(topItem->lastEvent, makeTouchData(QEvent::TouchEnd, window, Qt::TouchPointReleased,
makeTouchPoint(topItem, topItem->mapFromItem(bottomItem, pos))));
COMPARE_TOUCH_DATA(bottomItem->lastEvent, makeTouchData(QEvent::TouchBegin, window, Qt::TouchPointPressed, makeTouchPoint(bottomItem, pos)));
topItem->reset();
bottomItem->reset();
delete topItem;
delete middleItem;
delete bottomItem;
}
void tst_qquickwindow::touchEvent_propagation()
{
TestTouchItem::clearMouseEventCounters();
QFETCH(bool, acceptTouchEvents);
QFETCH(bool, acceptMouseEvents);
QFETCH(bool, enableItem);
QFETCH(bool, showItem);
QQuickWindow *window = new QQuickWindow;
QScopedPointer<QQuickWindow> cleanup(window);
window->resize(250, 250);
window->setPosition(100, 100);
window->setTitle(QTest::currentTestFunction());
window->show();
QVERIFY(QTest::qWaitForWindowActive(window));
TestTouchItem *bottomItem = new TestTouchItem(window->contentItem());
bottomItem->setObjectName("Bottom Item");
bottomItem->setSize(QSizeF(150, 150));
TestTouchItem *middleItem = new TestTouchItem(bottomItem);
middleItem->setObjectName("Middle Item");
middleItem->setPosition(QPointF(50, 50));
middleItem->setSize(QSizeF(150, 150));
TestTouchItem *topItem = new TestTouchItem(middleItem);
topItem->setObjectName("Top Item");
topItem->setPosition(QPointF(50, 50));
topItem->setSize(QSizeF(150, 150));
QPointF pos(10, 10);
QPoint pointInBottomItem = bottomItem->mapToScene(pos).toPoint(); // (10, 10)
QPoint pointInMiddleItem = middleItem->mapToScene(pos).toPoint(); // (60, 60) overlaps with bottomItem
QPoint pointInTopItem = topItem->mapToScene(pos).toPoint(); // (110, 110) overlaps with bottom & top items
// disable topItem
topItem->acceptTouchEvents = acceptTouchEvents;
topItem->acceptMouseEvents = acceptMouseEvents;
topItem->setEnabled(enableItem);
topItem->setVisible(showItem);
// single touch to top item, should be received by middle item
QTest::touchEvent(window, touchDevice).press(0, pointInTopItem, window);
QTRY_COMPARE(middleItem->lastEvent.touchPoints.count(), 1);
QVERIFY(topItem->lastEvent.touchPoints.isEmpty());
QVERIFY(bottomItem->lastEvent.touchPoints.isEmpty());
COMPARE_TOUCH_DATA(middleItem->lastEvent, makeTouchData(QEvent::TouchBegin, window, Qt::TouchPointPressed,
makeTouchPoint(middleItem, middleItem->mapFromItem(topItem, pos))));
QTest::touchEvent(window, touchDevice).release(0, pointInTopItem, window);
// touch top and middle items, middle item should get both events
QTest::touchEvent(window, touchDevice).press(0, pointInTopItem, window)
.press(1, pointInMiddleItem, window);
QTRY_COMPARE(middleItem->lastEvent.touchPoints.count(), 2);
QVERIFY(topItem->lastEvent.touchPoints.isEmpty());
QVERIFY(bottomItem->lastEvent.touchPoints.isEmpty());
COMPARE_TOUCH_DATA(middleItem->lastEvent, makeTouchData(QEvent::TouchBegin, window, Qt::TouchPointPressed,
(QList<QTouchEvent::TouchPoint>() << makeTouchPoint(middleItem, middleItem->mapFromItem(topItem, pos))
<< makeTouchPoint(middleItem, pos) )));
QTest::touchEvent(window, touchDevice).release(0, pointInTopItem, window)
.release(1, pointInMiddleItem, window);
middleItem->reset();
// disable middleItem as well
middleItem->acceptTouchEvents = acceptTouchEvents;
middleItem->acceptMouseEvents = acceptMouseEvents;
middleItem->setEnabled(enableItem);
middleItem->setVisible(showItem);
// touch top and middle items, bottom item should get all events
QTest::touchEvent(window, touchDevice).press(0, pointInTopItem, window)
.press(1, pointInMiddleItem, window);
QTRY_COMPARE(bottomItem->lastEvent.touchPoints.count(), 2);
QVERIFY(topItem->lastEvent.touchPoints.isEmpty());
QVERIFY(middleItem->lastEvent.touchPoints.isEmpty());
COMPARE_TOUCH_DATA(bottomItem->lastEvent, makeTouchData(QEvent::TouchBegin, window, Qt::TouchPointPressed,
(QList<QTouchEvent::TouchPoint>() << makeTouchPoint(bottomItem, bottomItem->mapFromItem(topItem, pos))
<< makeTouchPoint(bottomItem, bottomItem->mapFromItem(middleItem, pos)) )));
bottomItem->reset();
// disable bottom item as well
bottomItem->acceptTouchEvents = acceptTouchEvents;
bottomItem->setEnabled(enableItem);
bottomItem->setVisible(showItem);
QTest::touchEvent(window, touchDevice).release(0, pointInTopItem, window)
.release(1, pointInMiddleItem, window);
// no events should be received
QTest::touchEvent(window, touchDevice).press(0, pointInTopItem, window)
.press(1, pointInMiddleItem, window)
.press(2, pointInBottomItem, window);
QTest::qWait(50);
QVERIFY(topItem->lastEvent.touchPoints.isEmpty());
QVERIFY(middleItem->lastEvent.touchPoints.isEmpty());
QVERIFY(bottomItem->lastEvent.touchPoints.isEmpty());
QTest::touchEvent(window, touchDevice).release(0, pointInTopItem, window)
.release(1, pointInMiddleItem, window)
.release(2, pointInBottomItem, window);
topItem->reset();
middleItem->reset();
bottomItem->reset();
// disable middle item, touch on top item
middleItem->acceptTouchEvents = acceptTouchEvents;
middleItem->setEnabled(enableItem);
middleItem->setVisible(showItem);
QTest::touchEvent(window, touchDevice).press(0, pointInTopItem, window);
QTest::qWait(50);
if (!enableItem || !showItem) {
// middle item is disabled or has 0 opacity, bottom item receives the event
QVERIFY(topItem->lastEvent.touchPoints.isEmpty());
QVERIFY(middleItem->lastEvent.touchPoints.isEmpty());
QCOMPARE(bottomItem->lastEvent.touchPoints.count(), 1);
COMPARE_TOUCH_DATA(bottomItem->lastEvent, makeTouchData(QEvent::TouchBegin, window, Qt::TouchPointPressed,
makeTouchPoint(bottomItem, bottomItem->mapFromItem(topItem, pos))));
} else {
// middle item ignores event, sends it to the top item (top-most child)
QCOMPARE(topItem->lastEvent.touchPoints.count(), 1);
QVERIFY(middleItem->lastEvent.touchPoints.isEmpty());
QVERIFY(bottomItem->lastEvent.touchPoints.isEmpty());
COMPARE_TOUCH_DATA(topItem->lastEvent, makeTouchData(QEvent::TouchBegin, window, Qt::TouchPointPressed,
makeTouchPoint(topItem, pos)));
}
QTest::touchEvent(window, touchDevice).release(0, pointInTopItem, window);
delete topItem;
delete middleItem;
delete bottomItem;
}
void tst_qquickwindow::touchEvent_propagation_data()
{
QTest::addColumn<bool>("acceptTouchEvents");
QTest::addColumn<bool>("acceptMouseEvents");
QTest::addColumn<bool>("enableItem");
QTest::addColumn<bool>("showItem");
QTest::newRow("disable events") << false << false << true << true;
QTest::newRow("disable item") << true << true << false << true;
QTest::newRow("hide item") << true << true << true << false;
}
void tst_qquickwindow::touchEvent_cancel()
{
TestTouchItem::clearMouseEventCounters();
QQuickWindow *window = new QQuickWindow;
QScopedPointer<QQuickWindow> cleanup(window);
window->resize(250, 250);
window->setPosition(100, 100);
window->setTitle(QTest::currentTestFunction());
window->show();
QVERIFY(QTest::qWaitForWindowActive(window));
TestTouchItem *item = new TestTouchItem(window->contentItem());
item->setPosition(QPointF(50, 50));
item->setSize(QSizeF(150, 150));
QPointF pos(50, 50);
QTest::touchEvent(window, touchDevice).press(0, item->mapToScene(pos).toPoint(), window);
QCoreApplication::processEvents();
QTRY_COMPARE(item->lastEvent.touchPoints.count(), 1);
TouchEventData d = makeTouchData(QEvent::TouchBegin, window, Qt::TouchPointPressed, makeTouchPoint(item, pos));
COMPARE_TOUCH_DATA(item->lastEvent, d);
item->reset();
QWindowSystemInterface::handleTouchCancelEvent(nullptr, touchDevice);
QCoreApplication::processEvents();
d = makeTouchData(QEvent::TouchCancel, window);
COMPARE_TOUCH_DATA(item->lastEvent, d);
delete item;
}
void tst_qquickwindow::touchEvent_cancelClearsMouseGrab()
{
TestTouchItem::clearMouseEventCounters();
QQuickWindow *window = new QQuickWindow;
QScopedPointer<QQuickWindow> cleanup(window);
window->resize(250, 250);
window->setPosition(100, 100);
window->setTitle(QTest::currentTestFunction());
window->show();
QVERIFY(QTest::qWaitForWindowActive(window));
TestTouchItem *item = new TestTouchItem(window->contentItem());
item->setPosition(QPointF(50, 50));
item->setSize(QSizeF(150, 150));
item->acceptMouseEvents = true;
item->acceptTouchEvents = false;
QPointF pos(50, 50);
QTest::touchEvent(window, touchDevice).press(0, item->mapToScene(pos).toPoint(), window);
QCoreApplication::processEvents();
QTRY_COMPARE(item->mousePressCount, 1);
QTRY_COMPARE(item->mouseUngrabEventCount, 0);
QWindowSystemInterface::handleTouchCancelEvent(nullptr, touchDevice);
QCoreApplication::processEvents();
QTRY_COMPARE(item->mouseUngrabEventCount, 1);
}
void tst_qquickwindow::touchEvent_reentrant()
{
TestTouchItem::clearMouseEventCounters();
QQuickWindow *window = new QQuickWindow;
QScopedPointer<QQuickWindow> cleanup(window);
window->resize(250, 250);
window->setPosition(100, 100);
window->setTitle(QTest::currentTestFunction());
window->show();
QVERIFY(QTest::qWaitForWindowActive(window));
TestTouchItem *item = new TestTouchItem(window->contentItem());
item->spinLoopWhenPressed = true; // will call processEvents() from the touch handler
item->setPosition(QPointF(50, 50));
item->setSize(QSizeF(150, 150));
QPointF pos(60, 60);
// None of these should commit from the dtor.
QTest::QTouchEventSequence press = QTest::touchEvent(window, touchDevice, false).press(0, pos.toPoint(), window);
pos += QPointF(2, 2);
QTest::QTouchEventSequence move = QTest::touchEvent(window, touchDevice, false).move(0, pos.toPoint(), window);
QTest::QTouchEventSequence release = QTest::touchEvent(window, touchDevice, false).release(0, pos.toPoint(), window);
// Now commit (i.e. call QWindowSystemInterface::handleTouchEvent), but do not process the events yet.
press.commit(false);
move.commit(false);
release.commit(false);
QCoreApplication::processEvents();
QTRY_COMPARE(item->touchEventCount, 3);
delete item;
}
void tst_qquickwindow::touchEvent_velocity()
{
TestTouchItem::clearMouseEventCounters();
QQuickWindow *window = new QQuickWindow;
QScopedPointer<QQuickWindow> cleanup(window);
window->resize(250, 250);
window->setPosition(100, 100);
window->setTitle(QTest::currentTestFunction());
window->show();
QVERIFY(QTest::qWaitForWindowExposed(window));
QTest::qWait(10);
TestTouchItem *item = new TestTouchItem(window->contentItem());
item->setPosition(QPointF(50, 50));
item->setSize(QSizeF(150, 150));
QList<QTouchEvent::TouchPoint> points;
QTouchEvent::TouchPoint tp;
tp.setId(1);
tp.setState(Qt::TouchPointPressed);
const QPointF localPos = item->mapToScene(QPointF(10, 10));
const QPointF screenPos = window->mapToGlobal(localPos.toPoint());
tp.setPos(localPos);
tp.setScreenPos(screenPos);
tp.setEllipseDiameters(QSizeF(4, 4));
points << tp;
QWindowSystemInterface::handleTouchEvent(window, touchDeviceWithVelocity,
QWindowSystemInterfacePrivate::toNativeTouchPoints(points, window));
QGuiApplication::processEvents();
QQuickTouchUtils::flush(window);
QCOMPARE(item->touchEventCount, 1);
points[0].setState(Qt::TouchPointMoved);
points[0].setPos(localPos + QPointF(5, 5));
points[0].setScreenPos(screenPos + QPointF(5, 5));
QVector2D velocity(1.5, 2.5);
points[0].setVelocity(velocity);
QWindowSystemInterface::handleTouchEvent(window, touchDeviceWithVelocity,
QWindowSystemInterfacePrivate::toNativeTouchPoints(points, window));
QGuiApplication::processEvents();
QQuickTouchUtils::flush(window);
QCOMPARE(item->touchEventCount, 2);
QCOMPARE(item->lastEvent.touchPoints.count(), 1);
QCOMPARE(item->lastVelocity, velocity);
// Now have a transformation on the item and check if velocity and position are transformed accordingly.
item->setRotation(90); // clockwise
QMatrix4x4 transformMatrix;
transformMatrix.rotate(-90, 0, 0, 1); // counterclockwise
QVector2D transformedVelocity = transformMatrix.mapVector(velocity).toVector2D();
points[0].setPos(points[0].pos() + QPointF(5, 5));
points[0].setScreenPos(points[0].screenPos() + QPointF(5, 5));
QWindowSystemInterface::handleTouchEvent(window, touchDeviceWithVelocity,
QWindowSystemInterfacePrivate::toNativeTouchPoints(points, window));
QGuiApplication::processEvents();
QQuickTouchUtils::flush(window);
QCOMPARE(item->lastVelocity, transformedVelocity);
QPoint itemLocalPos = item->mapFromScene(points[0].pos()).toPoint();
QPoint itemLocalPosFromEvent = item->lastEvent.touchPoints[0].pos().toPoint();
QCOMPARE(itemLocalPos, itemLocalPosFromEvent);
points[0].setState(Qt::TouchPointReleased);
QWindowSystemInterface::handleTouchEvent(window, touchDeviceWithVelocity,
QWindowSystemInterfacePrivate::toNativeTouchPoints(points, window));
QGuiApplication::processEvents();
QQuickTouchUtils::flush(window);
delete item;
}
void tst_qquickwindow::mergeTouchPointLists_data()
{
QTest::addColumn<QVector<QQuickItem*>>("list1");
QTest::addColumn<QVector<QQuickItem*>>("list2");
QTest::addColumn<QVector<QQuickItem*>>("expected");
QTest::addColumn<bool>("showItem");
// FIXME: do not leak all these items
auto item1 = new QQuickItem();
auto item2 = new QQuickItem();
auto item3 = new QQuickItem();
auto item4 = new QQuickItem();
auto item5 = new QQuickItem();
QTest::newRow("empty") << QVector<QQuickItem*>() << QVector<QQuickItem*>() << QVector<QQuickItem*>();
QTest::newRow("single list left")
<< (QVector<QQuickItem*>() << item1 << item2 << item3)
<< QVector<QQuickItem*>()
<< (QVector<QQuickItem*>() << item1 << item2 << item3);
QTest::newRow("single list right")
<< QVector<QQuickItem*>()
<< (QVector<QQuickItem*>() << item1 << item2 << item3)
<< (QVector<QQuickItem*>() << item1 << item2 << item3);
QTest::newRow("two lists identical")
<< (QVector<QQuickItem*>() << item1 << item2 << item3)
<< (QVector<QQuickItem*>() << item1 << item2 << item3)
<< (QVector<QQuickItem*>() << item1 << item2 << item3);
QTest::newRow("two lists 1")
<< (QVector<QQuickItem*>() << item1 << item2 << item5)
<< (QVector<QQuickItem*>() << item3 << item4 << item5)
<< (QVector<QQuickItem*>() << item1 << item2 << item3 << item4 << item5);
QTest::newRow("two lists 2")
<< (QVector<QQuickItem*>() << item1 << item2 << item5)
<< (QVector<QQuickItem*>() << item3 << item4 << item5)
<< (QVector<QQuickItem*>() << item1 << item2 << item3 << item4 << item5);
QTest::newRow("two lists 3")
<< (QVector<QQuickItem*>() << item1 << item2 << item3)
<< (QVector<QQuickItem*>() << item1 << item4 << item5)
<< (QVector<QQuickItem*>() << item1 << item2 << item3 << item4 << item5);
QTest::newRow("two lists 4")
<< (QVector<QQuickItem*>() << item1 << item3 << item4)
<< (QVector<QQuickItem*>() << item2 << item3 << item5)
<< (QVector<QQuickItem*>() << item1 << item2 << item3 << item4 << item5);
QTest::newRow("two lists 5")
<< (QVector<QQuickItem*>() << item1 << item2 << item4)
<< (QVector<QQuickItem*>() << item1 << item3 << item4)
<< (QVector<QQuickItem*>() << item1 << item2 << item3 << item4);
}
void tst_qquickwindow::mergeTouchPointLists()
{
QFETCH(QVector<QQuickItem*>, list1);
QFETCH(QVector<QQuickItem*>, list2);
QFETCH(QVector<QQuickItem*>, expected);
QQuickWindow win;
auto windowPrivate = QQuickWindowPrivate::get(&win);
auto targetList = windowPrivate->mergePointerTargets(list1, list2);
QCOMPARE(targetList, expected);
}
void tst_qquickwindow::mouseFromTouch_basic()
{
// Turn off accepting touch events with acceptTouchEvents. This
// should result in sending mouse events generated from the touch
// with the new event propagation system.
TestTouchItem::clearMouseEventCounters();
QQuickWindow *window = new QQuickWindow;
QScopedPointer<QQuickWindow> cleanup(window);
window->resize(250, 250);
window->setPosition(100, 100);
window->setTitle(QTest::currentTestFunction());
window->show();
QVERIFY(QTest::qWaitForWindowExposed(window));
QTest::qWait(10);
TestTouchItem *item = new TestTouchItem(window->contentItem());
item->setPosition(QPointF(50, 50));
item->setSize(QSizeF(150, 150));
item->acceptTouchEvents = false;
QList<QTouchEvent::TouchPoint> points;
QTouchEvent::TouchPoint tp;
tp.setId(1);
tp.setState(Qt::TouchPointPressed);
const QPointF localPos = item->mapToScene(QPointF(10, 10));
const QPointF screenPos = window->mapToGlobal(localPos.toPoint());
tp.setPos(localPos);
tp.setScreenPos(screenPos);
tp.setEllipseDiameters(QSizeF(4, 4));
points << tp;
QWindowSystemInterface::handleTouchEvent(window, touchDeviceWithVelocity,
QWindowSystemInterfacePrivate::toNativeTouchPoints(points, window));
QGuiApplication::processEvents();
QQuickTouchUtils::flush(window);
points[0].setState(Qt::TouchPointMoved);
points[0].setPos(localPos + QPointF(5, 5));
points[0].setScreenPos(screenPos + QPointF(5, 5));
QVector2D velocity(1.5, 2.5);
points[0].setVelocity(velocity);
QWindowSystemInterface::handleTouchEvent(window, touchDeviceWithVelocity,
QWindowSystemInterfacePrivate::toNativeTouchPoints(points, window));
QGuiApplication::processEvents();
QQuickTouchUtils::flush(window);
points[0].setState(Qt::TouchPointReleased);
QWindowSystemInterface::handleTouchEvent(window, touchDeviceWithVelocity,
QWindowSystemInterfacePrivate::toNativeTouchPoints(points, window));
QGuiApplication::processEvents();
QQuickTouchUtils::flush(window);
// The item should have received a mouse press, move, and release.
QCOMPARE(item->mousePressNum, 1);
QCOMPARE(item->mouseMoveNum, 1);
QCOMPARE(item->mouseReleaseNum, 1);
QCOMPARE(item->lastMousePos.toPoint(), item->mapFromScene(points[0].pos()).toPoint());
QCOMPARE(item->lastVelocityFromMouseMove, velocity);
QVERIFY((item->lastMouseCapabilityFlags & QTouchDevice::Velocity) != 0);
// Now the same with a transformation.
item->setRotation(90); // clockwise
QMatrix4x4 transformMatrix;
transformMatrix.rotate(-90, 0, 0, 1); // counterclockwise
QVector2D transformedVelocity = transformMatrix.mapVector(velocity).toVector2D();
points[0].setState(Qt::TouchPointPressed);
points[0].setVelocity(velocity);
tp.setPos(localPos);
tp.setScreenPos(screenPos);
QWindowSystemInterface::handleTouchEvent(window, touchDeviceWithVelocity,
QWindowSystemInterfacePrivate::toNativeTouchPoints(points, window));
QGuiApplication::processEvents();
QQuickTouchUtils::flush(window);
points[0].setState(Qt::TouchPointMoved);
points[0].setPos(localPos + QPointF(5, 5));
points[0].setScreenPos(screenPos + QPointF(5, 5));
QWindowSystemInterface::handleTouchEvent(window, touchDeviceWithVelocity,
QWindowSystemInterfacePrivate::toNativeTouchPoints(points, window));
QGuiApplication::processEvents();
QQuickTouchUtils::flush(window);
QCOMPARE(item->lastMousePos.toPoint(), item->mapFromScene(points[0].pos()).toPoint());
QCOMPARE(item->lastVelocityFromMouseMove, transformedVelocity);
points[0].setState(Qt::TouchPointReleased);
QWindowSystemInterface::handleTouchEvent(window, touchDeviceWithVelocity,
QWindowSystemInterfacePrivate::toNativeTouchPoints(points, window));
QCoreApplication::processEvents();
QQuickTouchUtils::flush(window);
delete item;
}
void tst_qquickwindow::synthMouseFromTouch_data()
{
QTest::addColumn<bool>("synthMouse"); // AA_SynthesizeMouseForUnhandledTouchEvents
QTest::addColumn<bool>("acceptTouch"); // QQuickItem::touchEvent: setAccepted()
QTest::newRow("no synth, accept") << false << true; // suitable for touch-capable UIs
QTest::newRow("no synth, don't accept") << false << false;
QTest::newRow("synth and accept") << true << true;
QTest::newRow("synth, don't accept") << true << false; // the default
}
void tst_qquickwindow::synthMouseFromTouch()
{
QFETCH(bool, synthMouse);
QFETCH(bool, acceptTouch);
QCoreApplication::setAttribute(Qt::AA_SynthesizeMouseForUnhandledTouchEvents, synthMouse);
QScopedPointer<MouseRecordingWindow> window(new MouseRecordingWindow);
QScopedPointer<MouseRecordingItem> item(new MouseRecordingItem(acceptTouch, nullptr));
item->setParentItem(window->contentItem());
window->resize(250, 250);
window->setPosition(100, 100);
window->setTitle(QTest::currentTestFunction());
window->show();
QVERIFY(QTest::qWaitForWindowActive(window.data()));
QPoint p1 = QPoint(20, 20);
QPoint p2 = QPoint(30, 30);
QTest::touchEvent(window.data(), touchDevice).press(0, p1, window.data());
QTest::touchEvent(window.data(), touchDevice).move(0, p2, window.data());
QTest::touchEvent(window.data(), touchDevice).release(0, p2, window.data());
QCOMPARE(item->m_touchEvents.count(), !synthMouse && !acceptTouch ? 1 : 3);
QCOMPARE(item->m_mouseEvents.count(), (acceptTouch || !synthMouse) ? 0 : 3);
QCOMPARE(window->m_mouseEvents.count(), 0);
for (const QMouseEvent &ev : item->m_mouseEvents)
QCOMPARE(ev.source(), Qt::MouseEventSynthesizedByQt);
}
void tst_qquickwindow::synthMouseDoubleClickFromTouch_data()
{
QTest::addColumn<QPoint>("movement");
QTest::addColumn<QPoint>("distanceBetweenPresses");
QTest::addColumn<bool>("expectedSynthesizedDoubleClickEvent");
QTest::newRow("normal") << QPoint(0, 0) << QPoint(0, 0) << true;
QTest::newRow("with 1 pixel wiggle") << QPoint(1, 1) << QPoint(1, 1) << true;
QTest::newRow("too much distance to second tap") << QPoint(0, 0) << QPoint(50, 0) << false;
QTest::newRow("too much drag") << QPoint(50, 0) << QPoint(0, 0) << false;
QTest::newRow("too much drag and too much distance to second tap") << QPoint(50, 0) << QPoint(50, 0) << false;
}
void tst_qquickwindow::synthMouseDoubleClickFromTouch()
{
QFETCH(QPoint, movement);
QFETCH(QPoint, distanceBetweenPresses);
QFETCH(bool, expectedSynthesizedDoubleClickEvent);
QCoreApplication::setAttribute(Qt::AA_SynthesizeMouseForUnhandledTouchEvents, true);
QScopedPointer<MouseRecordingWindow> window(new MouseRecordingWindow);
QScopedPointer<MouseRecordingItem> item(new MouseRecordingItem(false, nullptr));
item->setParentItem(window->contentItem());
window->resize(250, 250);
window->setPosition(100, 100);
window->setTitle(QTest::currentTestFunction());
window->show();
QVERIFY(QTest::qWaitForWindowActive(window.data()));
QTest::qWait(100);
QPoint p1 = item->mapToScene(item->clipRect().center()).toPoint();
QTest::touchEvent(window.data(), touchDevice).press(0, p1, window.data());
QTest::touchEvent(window.data(), touchDevice).move(0, p1 + movement, window.data());
QTest::touchEvent(window.data(), touchDevice).release(0, p1 + movement, window.data());
QPoint p2 = p1 + distanceBetweenPresses;
QTest::touchEvent(window.data(), touchDevice).press(1, p2, window.data());
QTest::touchEvent(window.data(), touchDevice).move(1, p2 + movement, window.data());
QTest::touchEvent(window.data(), touchDevice).release(1, p2 + movement, window.data());
const int eventCount = item->m_mouseEvents.count();
QVERIFY(eventCount >= 2);
const int nDoubleClicks = std::count_if(item->m_mouseEvents.constBegin(), item->m_mouseEvents.constEnd(), [](const QMouseEvent &ev) { return (ev.type() == QEvent::MouseButtonDblClick); } );
const bool foundDoubleClick = (nDoubleClicks == 1);
QCOMPARE(foundDoubleClick, expectedSynthesizedDoubleClickEvent);
}
void tst_qquickwindow::clearWindow()
{
QQuickWindow *window = new QQuickWindow;
window->setTitle(QTest::currentTestFunction());
QQuickItem *item = new QQuickItem;
item->setParentItem(window->contentItem());
QCOMPARE(item->window(), window);
delete window;
QVERIFY(!item->window());
delete item;
}
void tst_qquickwindow::mouseFiltering()
{
TestTouchItem::clearMouseEventCounters();
QQuickWindow *window = new QQuickWindow;
QScopedPointer<QQuickWindow> cleanup(window);
window->resize(250, 250);
window->setPosition(100, 100);
window->setTitle(QTest::currentTestFunction());
window->show();
QVERIFY(QTest::qWaitForWindowActive(window));
TestTouchItem *bottomItem = new TestTouchItem(window->contentItem());
bottomItem->setObjectName("Bottom Item");
bottomItem->setSize(QSizeF(150, 150));
TestTouchItem *siblingItem = new TestTouchItem(bottomItem);
siblingItem->setObjectName("Sibling of Middle Item");
siblingItem->setPosition(QPointF(90, 25));
siblingItem->setSize(QSizeF(150, 150));
TestTouchItem *middleItem = new TestTouchItem(bottomItem);
middleItem->setObjectName("Middle Item");
middleItem->setPosition(QPointF(50, 50));
middleItem->setSize(QSizeF(150, 150));
TestTouchItem *topItem = new TestTouchItem(middleItem);
topItem->setObjectName("Top Item");
topItem->setPosition(QPointF(50, 50));
topItem->setSize(QSizeF(150, 150));
QPoint pos(100, 100);
QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, pos);
// Mouse filtering propagates down the stack, so the
// correct order is
// 1. middleItem filters event
// 2. bottomItem filters event
// 3. topItem receives event
QTRY_COMPARE(middleItem->mousePressCount, 1);
QTRY_COMPARE(bottomItem->mousePressCount, 2);
QTRY_COMPARE(topItem->mousePressCount, 3);
QCOMPARE(siblingItem->mousePressCount, 0);
QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, pos);
topItem->clearMouseEventCounters();
middleItem->clearMouseEventCounters();
bottomItem->clearMouseEventCounters();
siblingItem->clearMouseEventCounters();
// Repeat, but this time have the top item accept the press
topItem->acceptMouseEvents = true;
QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, pos);
// Mouse filtering propagates down the stack, so the
// correct order is
// 1. middleItem filters event
// 2. bottomItem filters event
// 3. topItem receives event
QTRY_COMPARE(middleItem->mousePressCount, 1);
QTRY_COMPARE(bottomItem->mousePressCount, 2);
QTRY_COMPARE(topItem->mousePressCount, 3);
QCOMPARE(siblingItem->mousePressCount, 0);
pos += QPoint(50, 50);
QTest::mouseMove(window, pos);
// The top item has grabbed, so the move goes there, but again
// all the ancestors can filter, even when the mouse is outside their bounds
QTRY_COMPARE(middleItem->mouseMoveCount, 1);
QTRY_COMPARE(bottomItem->mouseMoveCount, 2);
QTRY_COMPARE(topItem->mouseMoveCount, 3);
QCOMPARE(siblingItem->mouseMoveCount, 0);
// clean up mouse press state for the next tests
QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, pos);
}
void tst_qquickwindow::qmlCreation()
{
QQmlEngine engine;
QQmlComponent component(&engine);
component.loadUrl(testFileUrl("window.qml"));
QObject *created = component.create();
QScopedPointer<QObject> cleanup(created);
QVERIFY(created);
QQuickWindow *window = qobject_cast<QQuickWindow*>(created);
QVERIFY(window);
QCOMPARE(window->color(), QColor(Qt::green));
QQuickItem *item = window->findChild<QQuickItem*>("item");
QVERIFY(item);
QCOMPARE(item->window(), window);
}
void tst_qquickwindow::qmlCreationWithScreen()
{
QQmlEngine engine;
QQmlComponent component(&engine);
component.loadUrl(testFileUrl("windowWithScreen.qml"));
QObject *created = component.create();
QScopedPointer<QObject> cleanup(created);
QVERIFY(created);
QQuickWindow *window = qobject_cast<QQuickWindow*>(created);
QVERIFY(window);
QCOMPARE(window->color(), QColor(Qt::green));
QQuickItem *item = window->findChild<QQuickItem*>("item");
QVERIFY(item);
QCOMPARE(item->window(), window);
}
void tst_qquickwindow::clearColor()
{
//::grab examines rendering to make sure it works visually
QQuickWindow *window = new QQuickWindow;
QScopedPointer<QQuickWindow> cleanup(window);
window->resize(250, 250);
window->setPosition(100, 100);
window->setColor(Qt::blue);
window->setTitle(QTest::currentTestFunction());
window->show();
QVERIFY(QTest::qWaitForWindowExposed(window));
QCOMPARE(window->color(), QColor(Qt::blue));
}
void tst_qquickwindow::defaultState()
{
QQmlEngine engine;
QQmlComponent component(&engine);
component.setData("import QtQuick 2.0; import QtQuick.Window 2.1; Window { }", QUrl());
QObject *created = component.create();
QScopedPointer<QObject> cleanup(created);
QVERIFY(created);
QQuickWindow *qmlWindow = qobject_cast<QQuickWindow*>(created);
QVERIFY(qmlWindow);
qmlWindow->setTitle(QTest::currentTestFunction());
QQuickWindow cppWindow;
cppWindow.show();
QVERIFY(QTest::qWaitForWindowExposed(&cppWindow));
QCOMPARE(qmlWindow->windowState(), cppWindow.windowState());
}
void tst_qquickwindow::grab_data()
{
QTest::addColumn<bool>("visible");
QTest::addColumn<bool>("alpha");
QTest::newRow("visible,opaque") << true << false;
QTest::newRow("invisible,opaque") << false << false;
QTest::newRow("visible,transparent") << true << true;
QTest::newRow("invisible,transparent") << false << true;
}
void tst_qquickwindow::grab()
{
if ((QGuiApplication::platformName() == QLatin1String("offscreen"))
|| (QGuiApplication::platformName() == QLatin1String("minimal")))
QSKIP("Skipping due to grabWindow not functional on offscreen/minimal platforms");
QFETCH(bool, visible);
QFETCH(bool, alpha);
QQuickWindow window;
window.setTitle(QLatin1String(QTest::currentTestFunction()) + QLatin1Char(' ') + QLatin1String(QTest::currentDataTag()));
if (alpha) {
window.setColor(QColor(0, 0, 0, 0));
} else {
window.setColor(Qt::red);
}
window.resize(250, 250);
if (visible) {
window.show();
QVERIFY(QTest::qWaitForWindowExposed(&window));
} else {
window.create();
}
QImage content = window.grabWindow();
QCOMPARE(content.width(), int(window.width() * window.devicePixelRatio()));
QCOMPARE(content.height(), int(window.height() * window.devicePixelRatio()));
if (alpha) {
QCOMPARE((uint) content.convertToFormat(QImage::Format_ARGB32_Premultiplied).pixel(0, 0), (uint) 0x00000000);
} else {
QCOMPARE((uint) content.convertToFormat(QImage::Format_RGB32).pixel(0, 0), (uint) 0xffff0000);
}
}
void tst_qquickwindow::multipleWindows()
{
QList<QQuickWindow *> windows;
QScopedPointer<QQuickWindow> cleanup[6];
for (int i=0; i<6; ++i) {
QQuickWindow *c = new QQuickWindow();
c->setTitle(QLatin1String(QTest::currentTestFunction()) + QString::number(i));
c->setColor(Qt::GlobalColor(Qt::red + i));
c->resize(300, 200);
c->setPosition(100 + i * 30, 100 + i * 20);
c->show();
windows << c;
cleanup[i].reset(c);
QVERIFY(QTest::qWaitForWindowExposed(c));
}
// move them
for (int i=0; i<windows.size(); ++i) {
QQuickWindow *c = windows.at(i);
c->setPosition(100 + i * 30, 100 + i * 20 + 100);
}
// resize them
for (int i=0; i<windows.size(); ++i) {
QQuickWindow *c = windows.at(i);
c->resize(200, 150);
}
}
void tst_qquickwindow::animationsWhileHidden()
{
QQmlEngine engine;
QQmlComponent component(&engine);
component.loadUrl(testFileUrl("AnimationsWhileHidden.qml"));
QObject *created = component.create();
QScopedPointer<QObject> cleanup(created);
QQuickWindow *window = qobject_cast<QQuickWindow*>(created);
QVERIFY(window);
window->setTitle(QTest::currentTestFunction());
QVERIFY(window->isVisible());
// Now hide the window and verify that it went off screen
window->hide();
QTest::qWait(10);
QVERIFY(!window->isVisible());
// Running animaiton should cause it to become visible again shortly.
QTRY_VERIFY(window->isVisible());
}
void tst_qquickwindow::headless()
{
QQmlEngine engine;
QQmlComponent component(&engine);
component.loadUrl(testFileUrl("Headless.qml"));
QObject *created = component.create();
QScopedPointer<QObject> cleanup(created);
QQuickWindow *window = qobject_cast<QQuickWindow*>(created);
window->setPersistentOpenGLContext(false);
window->setPersistentSceneGraph(false);
QVERIFY(window);
window->setTitle(QTest::currentTestFunction());
window->show();
QVERIFY(QTest::qWaitForWindowExposed(window));
QVERIFY(window->isVisible());
const bool threaded = QQuickWindowPrivate::get(window)->context->thread() != QThread::currentThread();
QSignalSpy initialized(window, SIGNAL(sceneGraphInitialized()));
QSignalSpy invalidated(window, SIGNAL(sceneGraphInvalidated()));
// Verify that the window is alive and kicking
QVERIFY(window->isSceneGraphInitialized());
const bool isGL = window->rendererInterface()->graphicsApi() == QSGRendererInterface::OpenGL;
// Store the visual result
QImage originalContent = window->grabWindow();
// Hide the window and verify signal emittion and GL context deletion
window->hide();
window->releaseResources();
if (threaded) {
QTRY_VERIFY(invalidated.size() >= 1);
if (isGL)
QVERIFY(!window->isSceneGraphInitialized());
}
// Destroy the native windowing system buffers
window->destroy();
QVERIFY(!window->handle());
// Show and verify that we are back and running
window->show();
QVERIFY(QTest::qWaitForWindowExposed(window));
if (threaded)
QTRY_COMPARE(initialized.size(), 1);
QVERIFY(window->isSceneGraphInitialized());
// Verify that the visual output is the same
QImage newContent = window->grabWindow();
QString errorMessage;
QVERIFY2(QQuickVisualTestUtil::compareImages(newContent, originalContent, &errorMessage),
qPrintable(errorMessage));
}
void tst_qquickwindow::noUpdateWhenNothingChanges()
{
QQuickWindow window;
window.setTitle(QTest::currentTestFunction());
window.setGeometry(100, 100, 300, 200);
QQuickRectangle rect(window.contentItem());
window.showNormal();
QVERIFY(QTest::qWaitForWindowExposed(&window));
// Many platforms are broken in the sense that that they follow up
// the initial expose with a second expose or more. Let these go
// through before we let the test continue.
QTest::qWait(100);
if (QQuickWindowPrivate::get(&window)->context->thread() == QGuiApplication::instance()->thread()) {
QSKIP("Only threaded renderloop implements this feature");
return;
}
QSignalSpy spy(&window, SIGNAL(frameSwapped()));
rect.update();
// Wait a while and verify that no more frameSwapped come our way.
QTest::qWait(100);
QCOMPARE(spy.size(), 0);
}
void tst_qquickwindow::focusObject()
{
QQmlEngine engine;
QQmlComponent component(&engine);
component.loadUrl(testFileUrl("focus.qml"));
QObject *created = component.create();
QScopedPointer<QObject> cleanup(created);
QVERIFY(created);
QQuickWindow *window = qobject_cast<QQuickWindow*>(created);
QVERIFY(window);
window->setTitle(QTest::currentTestFunction());
QSignalSpy focusObjectSpy(window, SIGNAL(focusObjectChanged(QObject*)));
window->show();
QVERIFY(QTest::qWaitForWindowExposed(window));
window->requestActivate();
QVERIFY(QTest::qWaitForWindowActive(window));
QCOMPARE(window->contentItem(), window->focusObject());
QCOMPARE(focusObjectSpy.count(), 1);
QQuickItem *item1 = window->findChild<QQuickItem*>("item1");
QVERIFY(item1);
item1->setFocus(true);
QCOMPARE(item1, window->focusObject());
QCOMPARE(focusObjectSpy.count(), 2);
QQuickItem *item2 = window->findChild<QQuickItem*>("item2");
QVERIFY(item2);
item2->setFocus(true);
QCOMPARE(item2, window->focusObject());
QCOMPARE(focusObjectSpy.count(), 3);
// set focus for item in non-focused focus scope and
// ensure focusObject does not change and signal is not emitted
QQuickItem *item3 = window->findChild<QQuickItem*>("item3");
QVERIFY(item3);
item3->setFocus(true);
QCOMPARE(item2, window->focusObject());
QCOMPARE(focusObjectSpy.count(), 3);
}
void tst_qquickwindow::focusReason()
{
QQuickWindow *window = new QQuickWindow;
QScopedPointer<QQuickWindow> cleanup(window);
window->resize(200, 200);
window->show();
window->setTitle(QTest::currentTestFunction());
QVERIFY(QTest::qWaitForWindowExposed(window));
QScopedPointer<QQuickItem> firstItem(new QQuickItem);
firstItem->setSize(QSizeF(100, 100));
firstItem->setParentItem(window->contentItem());
QScopedPointer<QQuickItem> secondItem(new QQuickItem);
secondItem->setSize(QSizeF(100, 100));
secondItem->setParentItem(window->contentItem());
firstItem->forceActiveFocus(Qt::OtherFocusReason);
QCOMPARE(QQuickWindowPrivate::get(window)->lastFocusReason, Qt::OtherFocusReason);
secondItem->forceActiveFocus(Qt::TabFocusReason);
QCOMPARE(QQuickWindowPrivate::get(window)->lastFocusReason, Qt::TabFocusReason);
firstItem->forceActiveFocus(Qt::BacktabFocusReason);
QCOMPARE(QQuickWindowPrivate::get(window)->lastFocusReason, Qt::BacktabFocusReason);
}
void tst_qquickwindow::ignoreUnhandledMouseEvents()
{
QQuickWindow *window = new QQuickWindow;
QScopedPointer<QQuickWindow> cleanup(window);
window->setTitle(QTest::currentTestFunction());
window->resize(100, 100);
window->show();
QVERIFY(QTest::qWaitForWindowExposed(window));
QScopedPointer<QQuickItem> item(new QQuickItem);
item->setSize(QSizeF(100, 100));
item->setParentItem(window->contentItem());
{
QMouseEvent me(QEvent::MouseButtonPress, QPointF(50, 50), Qt::LeftButton, Qt::LeftButton,
Qt::NoModifier);
me.setAccepted(true);
QVERIFY(QCoreApplication::sendEvent(window, &me));
QVERIFY(!me.isAccepted());
}
{
QMouseEvent me(QEvent::MouseMove, QPointF(51, 51), Qt::LeftButton, Qt::LeftButton,
Qt::NoModifier);
me.setAccepted(true);
QVERIFY(QCoreApplication::sendEvent(window, &me));
QVERIFY(!me.isAccepted());
}
{
QMouseEvent me(QEvent::MouseButtonRelease, QPointF(51, 51), Qt::LeftButton, Qt::LeftButton,
Qt::NoModifier);
me.setAccepted(true);
QVERIFY(QCoreApplication::sendEvent(window, &me));
QVERIFY(!me.isAccepted());
}
}
void tst_qquickwindow::ownershipRootItem()
{
qmlRegisterType<RootItemAccessor>("Test", 1, 0, "RootItemAccessor");
QQmlEngine engine;
QQmlComponent component(&engine);
component.loadUrl(testFileUrl("ownershipRootItem.qml"));
QObject *created = component.create();
QScopedPointer<QObject> cleanup(created);
QQuickWindow *window = qobject_cast<QQuickWindow*>(created);
QVERIFY(window);
window->setTitle(QTest::currentTestFunction());
window->show();
QVERIFY(QTest::qWaitForWindowExposed(window));
RootItemAccessor* accessor = window->findChild<RootItemAccessor*>("accessor");
QVERIFY(accessor);
engine.collectGarbage();
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
QCoreApplication::processEvents();
QVERIFY(!accessor->isRootItemDestroyed());
}
#if QT_CONFIG(cursor)
void tst_qquickwindow::cursor()
{
QQuickWindow window;
window.setTitle(QTest::currentTestFunction());
window.setFramePosition(QGuiApplication::primaryScreen()->availableGeometry().topLeft() + QPoint(50, 50));
window.resize(320, 290);
QQuickItem parentItem;
parentItem.setObjectName("parentItem");
parentItem.setPosition(QPointF(0, 0));
parentItem.setSize(QSizeF(180, 180));
parentItem.setParentItem(window.contentItem());
QQuickItem childItem;
childItem.setObjectName("childItem");
childItem.setPosition(QPointF(60, 90));
childItem.setSize(QSizeF(120, 120));
childItem.setParentItem(&parentItem);
QQuickItem clippingItem;
clippingItem.setObjectName("clippingItem");
clippingItem.setPosition(QPointF(120, 120));
clippingItem.setSize(QSizeF(180, 180));
clippingItem.setClip(true);
clippingItem.setParentItem(window.contentItem());
QQuickItem clippedItem;
clippedItem.setObjectName("clippedItem");
clippedItem.setPosition(QPointF(-30, -30));
clippedItem.setSize(QSizeF(120, 120));
clippedItem.setParentItem(&clippingItem);
window.show();
QVERIFY(QTest::qWaitForWindowActive(&window));
// Position the cursor over the parent and child item and the clipped section of clippedItem.
QTest::mouseMove(&window, QPoint(100, 100));
// No items cursors, window cursor is the default arrow.
QCOMPARE(window.cursor().shape(), Qt::ArrowCursor);
// The section of clippedItem under the cursor is clipped, and so doesn't affect the window cursor.
clippedItem.setCursor(Qt::ForbiddenCursor);
QCOMPARE(clippedItem.cursor().shape(), Qt::ForbiddenCursor);
QCOMPARE(window.cursor().shape(), Qt::ArrowCursor);
// parentItem is under the cursor, so the window cursor is changed.
parentItem.setCursor(Qt::IBeamCursor);
QCOMPARE(parentItem.cursor().shape(), Qt::IBeamCursor);
QCOMPARE(window.cursor().shape(), Qt::IBeamCursor);
// childItem is under the cursor and is in front of its parent, so the window cursor is changed.
childItem.setCursor(Qt::WaitCursor);
QCOMPARE(childItem.cursor().shape(), Qt::WaitCursor);
QCOMPARE(window.cursor().shape(), Qt::WaitCursor);
childItem.setCursor(Qt::PointingHandCursor);
QCOMPARE(childItem.cursor().shape(), Qt::PointingHandCursor);
QCOMPARE(window.cursor().shape(), Qt::PointingHandCursor);
// childItem is the current cursor item, so this has no effect on the window cursor.
parentItem.unsetCursor();
QCOMPARE(parentItem.cursor().shape(), Qt::ArrowCursor);
QCOMPARE(window.cursor().shape(), Qt::PointingHandCursor);
parentItem.setCursor(Qt::IBeamCursor);
QCOMPARE(parentItem.cursor().shape(), Qt::IBeamCursor);
QCOMPARE(window.cursor().shape(), Qt::PointingHandCursor);
// With the childItem cursor cleared, parentItem is now foremost.
childItem.unsetCursor();
QCOMPARE(childItem.cursor().shape(), Qt::ArrowCursor);
QCOMPARE(window.cursor().shape(), Qt::IBeamCursor);
// Setting the childItem cursor to the default still takes precedence over parentItem.
childItem.setCursor(Qt::ArrowCursor);
QCOMPARE(childItem.cursor().shape(), Qt::ArrowCursor);
QCOMPARE(window.cursor().shape(), Qt::ArrowCursor);
childItem.setCursor(Qt::WaitCursor);
QCOMPARE(childItem.cursor().shape(), Qt::WaitCursor);
QCOMPARE(window.cursor().shape(), Qt::WaitCursor);
// Move the cursor so it is over just parentItem.
QTest::mouseMove(&window, QPoint(20, 20));
QCOMPARE(window.cursor().shape(), Qt::IBeamCursor);
// Move the cursor so that is over all items, clippedItem wins because its a child of
// clippingItem which is in from of parentItem in painting order.
QTest::mouseMove(&window, QPoint(125, 125));
QCOMPARE(window.cursor().shape(), Qt::ForbiddenCursor);
// Over clippingItem only, so no cursor.
QTest::mouseMove(&window, QPoint(200, 280));
QCOMPARE(window.cursor().shape(), Qt::ArrowCursor);
// Over no item, so no cursor.
QTest::mouseMove(&window, QPoint(10, 280));
QCOMPARE(window.cursor().shape(), Qt::ArrowCursor);
// back to the start.
QTest::mouseMove(&window, QPoint(100, 100));
QCOMPARE(window.cursor().shape(), Qt::WaitCursor);
// Try with the mouse pressed.
QTest::mousePress(&window, Qt::LeftButton, Qt::NoModifier, QPoint(100, 100));
QTest::mouseMove(&window, QPoint(20, 20));
QCOMPARE(window.cursor().shape(), Qt::IBeamCursor);
QTest::mouseMove(&window, QPoint(125, 125));
QCOMPARE(window.cursor().shape(), Qt::ForbiddenCursor);
QTest::mouseMove(&window, QPoint(200, 280));
QCOMPARE(window.cursor().shape(), Qt::ArrowCursor);
QTest::mouseMove(&window, QPoint(10, 280));
QCOMPARE(window.cursor().shape(), Qt::ArrowCursor);
QTest::mouseMove(&window, QPoint(100, 100));
QCOMPARE(window.cursor().shape(), Qt::WaitCursor);
QTest::mouseRelease(&window, Qt::LeftButton, Qt::NoModifier, QPoint(100, 100));
// Remove the cursor item from the scene. Theoretically this should make parentItem the
// cursorItem, but given the situation will correct itself after the next mouse move it
// simply unsets the window cursor for now.
childItem.setParentItem(nullptr);
QCOMPARE(window.cursor().shape(), Qt::ArrowCursor);
parentItem.setCursor(Qt::SizeAllCursor);
QCOMPARE(parentItem.cursor().shape(), Qt::SizeAllCursor);
QCOMPARE(window.cursor().shape(), Qt::ArrowCursor);
// Changing the cursor of an un-parented item doesn't affect the window's cursor.
childItem.setCursor(Qt::ClosedHandCursor);
QCOMPARE(childItem.cursor().shape(), Qt::ClosedHandCursor);
QCOMPARE(window.cursor().shape(), Qt::ArrowCursor);
childItem.unsetCursor();
QCOMPARE(childItem.cursor().shape(), Qt::ArrowCursor);
QCOMPARE(window.cursor().shape(), Qt::ArrowCursor);
QTest::mouseRelease(&window, Qt::LeftButton, Qt::NoModifier, QPoint(100, 101));
QCOMPARE(window.cursor().shape(), Qt::SizeAllCursor);
}
#endif
void tst_qquickwindow::hideThenDelete_data()
{
QTest::addColumn<bool>("persistentSG");
QTest::addColumn<bool>("persistentGL");
QTest::newRow("persistent:SG=false,GL=false") << false << false;
QTest::newRow("persistent:SG=true,GL=false") << true << false;
QTest::newRow("persistent:SG=false,GL=true") << false << true;
QTest::newRow("persistent:SG=true,GL=true") << true << true;
}
void tst_qquickwindow::hideThenDelete()
{
QFETCH(bool, persistentSG);
QFETCH(bool, persistentGL);
QScopedPointer<QSignalSpy> openglDestroyed;
QScopedPointer<QSignalSpy> sgInvalidated;
{
QQuickWindow window;
window.setTitle(QLatin1String(QTest::currentTestFunction()) + QLatin1Char(' ')
+ QLatin1String(QTest::currentDataTag()));
window.setColor(Qt::red);
window.setPersistentSceneGraph(persistentSG);
window.setPersistentOpenGLContext(persistentGL);
window.resize(400, 300);
window.show();
QVERIFY(QTest::qWaitForWindowExposed(&window));
const bool threaded = QQuickWindowPrivate::get(&window)->context->thread() != QGuiApplication::instance()->thread();
const bool isGL = window.rendererInterface()->graphicsApi() == QSGRendererInterface::OpenGL;
#if QT_CONFIG(opengl)
if (isGL)
openglDestroyed.reset(new QSignalSpy(window.openglContext(), SIGNAL(aboutToBeDestroyed())));
#endif
sgInvalidated.reset(new QSignalSpy(&window, SIGNAL(sceneGraphInvalidated())));
window.hide();
QTRY_VERIFY(!window.isExposed());
if (threaded) {
if (!isGL)
QSKIP("Skipping persistency verification due to not running with OpenGL");
if (!persistentSG) {
QVERIFY(sgInvalidated->size() > 0);
if (!persistentGL)
QVERIFY(openglDestroyed->size() > 0);
else
QCOMPARE(openglDestroyed->size(), 0);
} else {
QCOMPARE(sgInvalidated->size(), 0);
QCOMPARE(openglDestroyed->size(), 0);
}
}
}
QVERIFY(sgInvalidated->size() > 0);
#if QT_CONFIG(opengl)
if (openglDestroyed)
QVERIFY(openglDestroyed->size() > 0);
#endif
}
void tst_qquickwindow::showHideAnimate()
{
// This test tries to mimick a bug triggered in the qquickanimatedimage test
// A window is shown, then removed again before it is exposed. This left
// traces in the render loop which prevent other animations from running
// later on.
{
QQuickWindow window;
window.resize(400, 300);
window.show();
}
QQmlEngine engine;
QQmlComponent component(&engine);
component.loadUrl(testFileUrl("showHideAnimate.qml"));
QScopedPointer<QQuickItem> created(qobject_cast<QQuickItem *>(component.create()));
QVERIFY(created);
QTRY_VERIFY(created->opacity() > 0.5);
QTRY_VERIFY(created->opacity() < 0.5);
}
void tst_qquickwindow::testExpose()
{
QQuickWindow window;
window.setTitle(QTest::currentTestFunction());
window.setGeometry(100, 100, 300, 200);
window.show();
QTRY_VERIFY(window.isExposed());
QSignalSpy swapSpy(&window, SIGNAL(frameSwapped()));
// exhaust pending exposes, as some platforms send us plenty
// while showing the first time
QTest::qWait(1000);
while (swapSpy.size() != 0) {
swapSpy.clear();
QTest::qWait(100);
}
QWindowSystemInterface::handleExposeEvent(&window, QRegion(10, 10, 20, 20));
QTRY_COMPARE(swapSpy.size(), 1);
}
void tst_qquickwindow::requestActivate()
{
QQmlEngine engine;
QQmlComponent component(&engine);
component.loadUrl(testFileUrl("active.qml"));
QScopedPointer<QQuickWindow> window1(qobject_cast<QQuickWindow *>(component.create()));
QVERIFY(!window1.isNull());
window1->setTitle(QTest::currentTestFunction());
QWindowList windows = QGuiApplication::topLevelWindows();
QCOMPARE(windows.size(), 2);
for (int i = 0; i < windows.size(); ++i) {
if (windows.at(i)->objectName() == window1->objectName()) {
windows.removeAt(i);
break;
}
}
QCOMPARE(windows.size(), 1);
QCOMPARE(windows.at(0)->objectName(), QLatin1String("window2"));
window1->show();
QVERIFY(QTest::qWaitForWindowExposed(windows.at(0))); //We wait till window 2 comes up
window1->requestActivate(); // and then transfer the focus to window1
QTRY_COMPARE(QGuiApplication::focusWindow(), window1.data());
QVERIFY(window1->isActive());
QQuickItem *item = QQuickVisualTestUtil::findItem<QQuickItem>(window1->contentItem(), "item1");
QVERIFY(item);
//copied from src/qmltest/quicktestevent.cpp
QPoint pos = item->mapToScene(QPointF(item->width()/2, item->height()/2)).toPoint();
QMouseEvent me(QEvent::MouseButtonPress, pos, window1->mapToGlobal(pos), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
QSpontaneKeyEvent::setSpontaneous(&me);
if (!qApp->notify(window1.data(), &me)) {
QString warning = QString::fromLatin1("Mouse event MousePress not accepted by receiving window");
QWARN(warning.toLatin1().data());
}
me = QMouseEvent(QEvent::MouseButtonPress, pos, window1->mapToGlobal(pos), Qt::LeftButton, {}, Qt::NoModifier);
QSpontaneKeyEvent::setSpontaneous(&me);
if (!qApp->notify(window1.data(), &me)) {
QString warning = QString::fromLatin1("Mouse event MouseRelease not accepted by receiving window");
QWARN(warning.toLatin1().data());
}
QTRY_COMPARE(QGuiApplication::focusWindow(), windows.at(0));
QVERIFY(windows.at(0)->isActive());
}
void tst_qquickwindow::testWindowVisibilityOrder()
{
QQmlEngine engine;
QQmlComponent component(&engine);
component.loadUrl(testFileUrl("windoworder.qml"));
QScopedPointer<QQuickWindow> window1(qobject_cast<QQuickWindow *>(component.create()));
QVERIFY(!window1.isNull());
window1->setTitle(QTest::currentTestFunction());
QQuickWindow *window2 = window1->property("win2").value<QQuickWindow*>();
QQuickWindow *window3 = window1->property("win3").value<QQuickWindow*>();
QQuickWindow *window4 = window1->property("win4").value<QQuickWindow*>();
QQuickWindow *window5 = window1->property("win5").value<QQuickWindow*>();
QVERIFY(window2);
QVERIFY(window3);
QVERIFY(QTest::qWaitForWindowExposed(window3));
QWindowList windows = QGuiApplication::topLevelWindows();
QTRY_COMPARE(windows.size(), 5);
if (qgetenv("XDG_CURRENT_DESKTOP") == "Unity" && QGuiApplication::focusWindow() != window3) {
qDebug() << "Unity (flaky QTBUG-62604): expected window3 to have focus; actual focusWindow:"
<< QGuiApplication::focusWindow();
} else {
QCOMPARE(window3, QGuiApplication::focusWindow());
QVERIFY(window1->isActive());
QVERIFY(window2->isActive());
QVERIFY(window3->isActive());
}
//Test if window4 is shown 2 seconds after the application startup
//with window4 visible window5 (transient child) should also become visible
QVERIFY(!window4->isVisible());
QVERIFY(!window5->isVisible());
window4->setVisible(true);
QVERIFY(QTest::qWaitForWindowExposed(window5));
QVERIFY(window4->isVisible());
QVERIFY(window5->isVisible());
}
void tst_qquickwindow::blockClosing()
{
QQmlEngine engine;
QQmlComponent component(&engine);
component.loadUrl(testFileUrl("ucantclosethis.qml"));
QScopedPointer<QQuickWindow> window(qobject_cast<QQuickWindow *>(component.create()));
QVERIFY(!window.isNull());
window->setTitle(QTest::currentTestFunction());
window->show();
QVERIFY(QTest::qWaitForWindowExposed(window.data()));
QVERIFY(window->isVisible());
QWindowSystemInterface::handleCloseEvent(window.data());
QVERIFY(window->isVisible());
QWindowSystemInterface::handleCloseEvent(window.data());
QVERIFY(window->isVisible());
window->setProperty("canCloseThis", true);
QWindowSystemInterface::handleCloseEvent(window.data());
QTRY_VERIFY(!window->isVisible());
}
void tst_qquickwindow::blockCloseMethod()
{
QQmlEngine engine;
QQmlComponent component(&engine);
component.loadUrl(testFileUrl("ucantclosethis.qml"));
QScopedPointer<QQuickWindow> window(qobject_cast<QQuickWindow *>(component.create()));
QVERIFY(!window.isNull());
window->setTitle(QTest::currentTestFunction());
window->show();
QVERIFY(QTest::qWaitForWindowExposed(window.data()));
QVERIFY(window->isVisible());
QVERIFY(QMetaObject::invokeMethod(window.data(), "close", Qt::DirectConnection));
QVERIFY(window->isVisible());
QVERIFY(QMetaObject::invokeMethod(window.data(), "close", Qt::DirectConnection));
QVERIFY(window->isVisible());
window->setProperty("canCloseThis", true);
QVERIFY(QMetaObject::invokeMethod(window.data(), "close", Qt::DirectConnection));
QTRY_VERIFY(!window->isVisible());
}
void tst_qquickwindow::crashWhenHoverItemDeleted()
{
// QTBUG-32771
QQmlEngine engine;
QQmlComponent component(&engine);
component.loadUrl(testFileUrl("hoverCrash.qml"));
QScopedPointer<QQuickWindow> window(qobject_cast<QQuickWindow *>(component.create()));
QVERIFY(!window.isNull());
window->setTitle(QTest::currentTestFunction());
window->show();
QVERIFY(QTest::qWaitForWindowActive(window.data()));
// Simulate a move from the first rectangle to the second. Crash will happen in here
// Moving instantaneously from (0, 99) to (0, 102) does not cause the crash
for (int i = 99; i < 102; ++i) {
QTest::mouseMove(window.data(), QPoint(0, i));
}
}
// QTBUG-33436
void tst_qquickwindow::unloadSubWindow()
{
QQmlEngine engine;
QQmlComponent component(&engine);
component.loadUrl(testFileUrl("unloadSubWindow.qml"));
QScopedPointer<QQuickWindow> window(qobject_cast<QQuickWindow *>(component.create()));
QVERIFY(!window.isNull());
window->setTitle(QTest::currentTestFunction());
window->show();
QVERIFY(QTest::qWaitForWindowExposed(window.data()));
QPointer<QQuickWindow> transient;
QTRY_VERIFY(transient = window->property("transientWindow").value<QQuickWindow*>());
QVERIFY(QTest::qWaitForWindowExposed(transient));
// Unload the inner window (in nested Loaders) and make sure it doesn't crash
QQuickLoader *loader = window->property("loader1").value<QQuickLoader*>();
loader->setActive(false);
QTRY_VERIFY(transient.isNull() || !transient->isVisible());
}
// QTBUG-52573
void tst_qquickwindow::changeVisibilityInCompleted()
{
QQmlEngine engine;
QQmlComponent component(&engine);
component.loadUrl(testFileUrl("changeVisibilityInCompleted.qml"));
QScopedPointer<QQuickWindow> window(qobject_cast<QQuickWindow *>(component.create()));
QVERIFY(!window.isNull());
window->setTitle(QTest::currentTestFunction());
window->show();
QVERIFY(QTest::qWaitForWindowExposed(window.data()));
QPointer<QQuickWindow> winVisible;
QTRY_VERIFY(winVisible = window->property("winVisible").value<QQuickWindow*>());
QPointer<QQuickWindow> winVisibility;
QTRY_VERIFY(winVisibility = window->property("winVisibility").value<QQuickWindow*>());
QVERIFY(QTest::qWaitForWindowExposed(winVisible));
QVERIFY(QTest::qWaitForWindowExposed(winVisibility));
QVERIFY(winVisible->isVisible());
QCOMPARE(winVisibility->visibility(), QWindow::Windowed);
}
// QTBUG-32004
void tst_qquickwindow::qobjectEventFilter_touch()
{
QQuickWindow window;
window.resize(250, 250);
window.setPosition(100, 100);
window.setTitle(QTest::currentTestFunction());
window.show();
QVERIFY(QTest::qWaitForWindowActive(&window));
TestTouchItem *item = new TestTouchItem(window.contentItem());
item->setSize(QSizeF(150, 150));
EventFilter eventFilter;
item->installEventFilter(&eventFilter);
QPointF pos(10, 10);
// press single point
QTest::touchEvent(&window, touchDevice).press(0, item->mapToScene(pos).toPoint(), &window);
QCOMPARE(eventFilter.events.count(), 1);
QCOMPARE(eventFilter.events.first(), (int)QEvent::TouchBegin);
}
// QTBUG-32004
void tst_qquickwindow::qobjectEventFilter_key()
{
QQuickWindow window;
window.resize(250, 250);
window.setPosition(100, 100);
window.setTitle(QTest::currentTestFunction());
window.show();
QVERIFY(QTest::qWaitForWindowActive(&window));
TestTouchItem *item = new TestTouchItem(window.contentItem());
item->setSize(QSizeF(150, 150));
item->setFocus(true);
EventFilter eventFilter;
item->installEventFilter(&eventFilter);
QTest::keyPress(&window, Qt::Key_A);
// NB: It may also receive some QKeyEvent(ShortcutOverride) which we're not interested in
QVERIFY(eventFilter.events.contains((int)QEvent::KeyPress));
eventFilter.events.clear();
QTest::keyRelease(&window, Qt::Key_A);
QVERIFY(eventFilter.events.contains((int)QEvent::KeyRelease));
}
// QTBUG-32004
void tst_qquickwindow::qobjectEventFilter_mouse()
{
QQuickWindow window;
window.resize(250, 250);
window.setPosition(100, 100);
window.setTitle(QTest::currentTestFunction());
window.show();
QVERIFY(QTest::qWaitForWindowActive(&window));
TestTouchItem *item = new TestTouchItem(window.contentItem());
item->setSize(QSizeF(150, 150));
EventFilter eventFilter;
item->installEventFilter(&eventFilter);
QPoint point = item->mapToScene(QPointF(10, 10)).toPoint();
QTest::mouseMove(&window, point);
QTest::mousePress(&window, Qt::LeftButton, Qt::NoModifier, point);
QVERIFY(eventFilter.events.contains((int)QEvent::MouseButtonPress));
// clean up mouse press state for the next tests
QTest::mouseRelease(&window, Qt::LeftButton, Qt::NoModifier, point);
}
void tst_qquickwindow::animatingSignal()
{
QQuickWindow window;
window.setTitle(QTest::currentTestFunction());
window.setGeometry(100, 100, 300, 200);
QSignalSpy spy(&window, SIGNAL(afterAnimating()));
window.show();
QTRY_VERIFY(window.isExposed());
QTRY_VERIFY(spy.count() > 1);
}
// QTBUG-36938
void tst_qquickwindow::contentItemSize()
{
QQuickWindow window;
window.setTitle(QTest::currentTestFunction());
QQuickItem *contentItem = window.contentItem();
QVERIFY(contentItem);
QCOMPARE(QSize(contentItem->width(), contentItem->height()), window.size());
QSizeF size(300, 200);
window.resize(size.toSize());
window.show();
QVERIFY(QTest::qWaitForWindowExposed(&window));
QCOMPARE(window.size(), size.toSize());
QCOMPARE(QSizeF(contentItem->width(), contentItem->height()), size);
QQmlEngine engine;
QQmlComponent component(&engine);
component.setData(QByteArray("import QtQuick 2.1\n Rectangle { anchors.fill: parent }"), QUrl());
QScopedPointer<QQuickItem> rect(qobject_cast<QQuickItem *>(component.create()));
QVERIFY(rect);
rect->setParentItem(window.contentItem());
QCOMPARE(QSizeF(rect->width(), rect->height()), size);
size.transpose();
window.resize(size.toSize());
QCOMPARE(window.size(), size.toSize());
// wait for resize event
QTRY_COMPARE(QSizeF(contentItem->width(), contentItem->height()), size);
QCOMPARE(QSizeF(rect->width(), rect->height()), size);
}
void tst_qquickwindow::defaultSurfaceFormat()
{
// It is quite difficult to verify anything for real since the resulting format after
// surface/context creation can be anything, depending on the platform and drivers,
// and many options and settings may fail in various configurations, but test at
// least using some harmless settings to check that the global, static format is
// taken into account in the requested format.
QSurfaceFormat savedDefaultFormat = QSurfaceFormat::defaultFormat();
// Verify that depth and stencil are set, as they should be, unless they are disabled
// via environment variables.
QSurfaceFormat format = savedDefaultFormat;
format.setSwapInterval(0);
format.setRedBufferSize(8);
format.setGreenBufferSize(8);
format.setBlueBufferSize(8);
format.setProfile(QSurfaceFormat::CompatibilityProfile);
format.setOption(QSurfaceFormat::DebugContext);
// Will not set depth and stencil. That should be added automatically,
// unless the are disabled (but they aren't).
QSurfaceFormat::setDefaultFormat(format);
QQuickWindow window;
window.setTitle(QTest::currentTestFunction());
window.show();
QVERIFY(QTest::qWaitForWindowExposed(&window));
if (window.rendererInterface()->graphicsApi() != QSGRendererInterface::OpenGL)
QSKIP("Skipping OpenGL context test due to not running with OpenGL");
const QSurfaceFormat reqFmt = window.requestedFormat();
QCOMPARE(format.swapInterval(), reqFmt.swapInterval());
QCOMPARE(format.redBufferSize(), reqFmt.redBufferSize());
QCOMPARE(format.greenBufferSize(), reqFmt.greenBufferSize());
QCOMPARE(format.blueBufferSize(), reqFmt.blueBufferSize());
QCOMPARE(format.profile(), reqFmt.profile());
QCOMPARE(int(format.options()), int(reqFmt.options()));
#if QT_CONFIG(opengl)
// Depth and stencil should be >= what has been requested. For real. But use
// the context since the window's surface format is only partially updated
// on most platforms.
const QOpenGLContext *openglContext = nullptr;
QTRY_VERIFY((openglContext = window.openglContext()) != nullptr);
QVERIFY(openglContext->format().depthBufferSize() >= 16);
QVERIFY(openglContext->format().stencilBufferSize() >= 8);
#endif
QSurfaceFormat::setDefaultFormat(savedDefaultFormat);
}
void tst_qquickwindow::attachedProperty()
{
QQuickView view(testFileUrl("windowattached.qml"));
view.setTitle(QTest::currentTestFunction());
view.show();
view.requestActivate();
QVERIFY(QTest::qWaitForWindowActive(&view));
QVERIFY(view.rootObject()->property("windowActive").toBool());
QCOMPARE(view.rootObject()->property("contentItem").value<QQuickItem*>(), view.contentItem());
QCOMPARE(view.rootObject()->property("windowWidth").toInt(), view.width());
QCOMPARE(view.rootObject()->property("windowHeight").toInt(), view.height());
QCOMPARE(view.rootObject()->property("window").value<QQuickView*>(), &view);
QQuickWindow *innerWindow = view.rootObject()->findChild<QQuickWindow*>("extraWindow");
QVERIFY(innerWindow);
innerWindow->show();
innerWindow->requestActivate();
QVERIFY(QTest::qWaitForWindowActive(innerWindow));
QQuickText *text = view.rootObject()->findChild<QQuickText*>("extraWindowText");
QVERIFY(text);
QCOMPARE(text->text(), QLatin1String("active\nvisibility: 2"));
QCOMPARE(text->property("contentItem").value<QQuickItem*>(), innerWindow->contentItem());
QCOMPARE(text->property("windowWidth").toInt(), innerWindow->width());
QCOMPARE(text->property("windowHeight").toInt(), innerWindow->height());
QCOMPARE(text->property("window").value<QQuickWindow*>(), innerWindow);
text->setParentItem(nullptr);
QVERIFY(!text->property("contentItem").value<QQuickItem*>());
QCOMPARE(text->property("windowWidth").toInt(), 0);
QCOMPARE(text->property("windowHeight").toInt(), 0);
QVERIFY(!text->property("window").value<QQuickWindow*>());
}
class RenderJob : public QRunnable
{
public:
RenderJob(QQuickWindow::RenderStage s, QList<QQuickWindow::RenderStage> *l) : stage(s), list(l) { }
~RenderJob() { ++deleted; }
QQuickWindow::RenderStage stage;
QList<QQuickWindow::RenderStage> *list;
void run() {
list->append(stage);
}
static int deleted;
};
#if QT_CONFIG(opengl)
class GlRenderJob : public QRunnable
{
public:
GlRenderJob(GLubyte *buf) : readPixel(buf), mutex(nullptr), condition(nullptr) {}
~GlRenderJob() {}
void run() {
QOpenGLContext::currentContext()->functions()->glClearColor(1.0f, 0, 0, 1.0f);
QOpenGLContext::currentContext()->functions()->glClear(GL_COLOR_BUFFER_BIT);
QOpenGLContext::currentContext()->functions()->glReadPixels(0, 0, 1, 1, GL_RGBA,
GL_UNSIGNED_BYTE,
(void *)readPixel);
if (mutex) {
mutex->lock();
condition->wakeOne();
mutex->unlock();
}
}
GLubyte *readPixel;
QMutex *mutex;
QWaitCondition *condition;
};
#endif
int RenderJob::deleted = 0;
void tst_qquickwindow::testRenderJob()
{
QList<QQuickWindow::RenderStage> completedJobs;
QQuickWindow::RenderStage stages[] = {
QQuickWindow::BeforeSynchronizingStage,
QQuickWindow::AfterSynchronizingStage,
QQuickWindow::BeforeRenderingStage,
QQuickWindow::AfterRenderingStage,
QQuickWindow::AfterSwapStage,
QQuickWindow::NoStage
};
const int numJobs = 6;
{
QQuickWindow window;
window.setTitle(QTest::currentTestFunction());
RenderJob::deleted = 0;
// Schedule the jobs
for (int i = 0; i < numJobs; ++i)
window.scheduleRenderJob(new RenderJob(stages[i], &completedJobs), stages[i]);
window.show();
QVERIFY(QTest::qWaitForWindowExposed(&window));
// All jobs should be deleted
QTRY_COMPARE(RenderJob::deleted, numJobs);
// The NoStage job is not completed, if it is issued when there is no context,
// but the rest will be queued and completed once relevant render stage is hit.
QCOMPARE(completedJobs.size(), numJobs - 1);
// Verify jobs were completed in correct order
for (int i = 0; i < numJobs - 1; ++i)
QCOMPARE(completedJobs.at(i), stages[i]);
// Check that NoStage job gets executed if it is scheduled when window is exposed
completedJobs.clear();
RenderJob::deleted = 0;
window.scheduleRenderJob(new RenderJob(QQuickWindow::NoStage, &completedJobs),
QQuickWindow::NoStage);
QTRY_COMPARE(RenderJob::deleted, 1);
if ((QGuiApplication::platformName() == QLatin1String("offscreen"))
|| (QGuiApplication::platformName() == QLatin1String("minimal")))
QEXPECT_FAIL("", "NoStage job fails on offscreen/minimal platforms", Continue);
QCOMPARE(completedJobs.size(), 1);
#if QT_CONFIG(opengl)
if (window.rendererInterface()->graphicsApi() == QSGRendererInterface::OpenGL) {
// Do a synchronized GL job.
GLubyte readPixel[4] = {0, 0, 0, 0};
GlRenderJob *glJob = new GlRenderJob(readPixel);
if (window.openglContext()->thread() != QThread::currentThread()) {
QMutex mutex;
QWaitCondition condition;
glJob->mutex = &mutex;
glJob->condition = &condition;
mutex.lock();
window.scheduleRenderJob(glJob, QQuickWindow::NoStage);
condition.wait(&mutex);
mutex.unlock();
} else {
window.scheduleRenderJob(glJob, QQuickWindow::NoStage);
}
QCOMPARE(int(readPixel[0]), 255);
QCOMPARE(int(readPixel[1]), 0);
QCOMPARE(int(readPixel[2]), 0);
QCOMPARE(int(readPixel[3]), 255);
}
#endif
}
// Verify that jobs are deleted when window is not rendered at all
completedJobs.clear();
RenderJob::deleted = 0;
{
QQuickWindow window2;
for (int i = 0; i < numJobs; ++i) {
window2.scheduleRenderJob(new RenderJob(stages[i], &completedJobs), stages[i]);
}
}
QTRY_COMPARE(RenderJob::deleted, numJobs);
QCOMPARE(completedJobs.size(), 0);
}
class EventCounter : public QQuickRectangle
{
public:
EventCounter(QQuickItem *parent = nullptr)
: QQuickRectangle(parent)
{ }
void addFilterEvent(QEvent::Type type)
{
m_returnTrueForType.append(type);
}
int childMouseEventFilterEventCount(QEvent::Type type)
{
return m_childMouseEventFilterEventCount.value(type, 0);
}
int eventCount(QEvent::Type type)
{
return m_eventCount.value(type, 0);
}
void reset()
{
m_eventCount.clear();
m_childMouseEventFilterEventCount.clear();
}
protected:
bool childMouseEventFilter(QQuickItem *, QEvent *event) override
{
m_childMouseEventFilterEventCount[event->type()]++;
return m_returnTrueForType.contains(event->type());
}
bool event(QEvent *event) override
{
m_eventCount[event->type()]++;
return QQuickRectangle::event(event);
}
private:
QList<QEvent::Type> m_returnTrueForType;
QMap<QEvent::Type, int> m_childMouseEventFilterEventCount;
QMap<QEvent::Type, int> m_eventCount;
};
void tst_qquickwindow::testHoverChildMouseEventFilter()
{
QQuickWindow window;
window.resize(250, 250);
window.setPosition(100, 100);
window.setTitle(QTest::currentTestFunction());
window.show();
QVERIFY(QTest::qWaitForWindowActive(&window));
EventCounter *bottomItem = new EventCounter(window.contentItem());
bottomItem->setObjectName("Bottom Item");
bottomItem->setSize(QSizeF(150, 150));
bottomItem->setAcceptHoverEvents(true);
EventCounter *middleItem = new EventCounter(bottomItem);
middleItem->setObjectName("Middle Item");
middleItem->setPosition(QPointF(50, 50));
middleItem->setSize(QSizeF(150, 150));
middleItem->setAcceptHoverEvents(true);
EventCounter *topItem = new EventCounter(middleItem);
topItem->setObjectName("Top Item");
topItem->setPosition(QPointF(50, 50));
topItem->setSize(QSizeF(150, 150));
topItem->setAcceptHoverEvents(true);
QPoint pos(10, 10);
QTest::mouseMove(&window, pos);
QTRY_VERIFY(bottomItem->eventCount(QEvent::HoverEnter) > 0);
QCOMPARE(bottomItem->childMouseEventFilterEventCount(QEvent::HoverEnter), 0);
QCOMPARE(middleItem->eventCount(QEvent::HoverEnter), 0);
QCOMPARE(topItem->eventCount(QEvent::HoverEnter), 0);
bottomItem->reset();
pos = QPoint(60, 60);
QTest::mouseMove(&window, pos);
QTRY_VERIFY(middleItem->eventCount(QEvent::HoverEnter) > 0);
QCOMPARE(bottomItem->childMouseEventFilterEventCount(QEvent::HoverEnter), 0);
middleItem->reset();
pos = QPoint(70,70);
bottomItem->setFiltersChildMouseEvents(true);
QTest::mouseMove(&window, pos);
QTRY_VERIFY(middleItem->eventCount(QEvent::HoverMove) > 0);
QVERIFY(bottomItem->childMouseEventFilterEventCount(QEvent::HoverMove) > 0);
QCOMPARE(topItem->eventCount(QEvent::HoverEnter), 0);
bottomItem->reset();
middleItem->reset();
pos = QPoint(110,110);
bottomItem->addFilterEvent(QEvent::HoverEnter);
QTest::mouseMove(&window, pos);
QTRY_VERIFY(bottomItem->childMouseEventFilterEventCount(QEvent::HoverEnter) > 0);
QCOMPARE(topItem->eventCount(QEvent::HoverEnter), 0);
QCOMPARE(middleItem->eventCount(QEvent::HoverEnter), 0);
}
class HoverTimestampConsumer : public QQuickItem
{
Q_OBJECT
public:
HoverTimestampConsumer(QQuickItem *parent = nullptr)
: QQuickItem(parent)
{
setAcceptHoverEvents(true);
}
void hoverEnterEvent(QHoverEvent *event) { hoverTimestamps << event->timestamp(); }
void hoverLeaveEvent(QHoverEvent *event) { hoverTimestamps << event->timestamp(); }
void hoverMoveEvent(QHoverEvent *event) { hoverTimestamps << event->timestamp(); }
QList<ulong> hoverTimestamps;
};
// Checks that a QHoverEvent carries the timestamp of the QMouseEvent that caused it.
// QTBUG-54600
void tst_qquickwindow::testHoverTimestamp()
{
QQuickWindow window;
window.resize(200, 200);
window.setPosition(100, 100);
window.setTitle(QTest::currentTestFunction());
window.show();
QVERIFY(QTest::qWaitForWindowActive(&window));
HoverTimestampConsumer *hoverConsumer = new HoverTimestampConsumer(window.contentItem());
hoverConsumer->setWidth(100);
hoverConsumer->setHeight(100);
hoverConsumer->setX(50);
hoverConsumer->setY(50);
// First position, outside
{
QMouseEvent mouseEvent(QEvent::MouseMove, QPointF(40, 40), QPointF(40, 40), QPointF(140, 140),
Qt::NoButton, Qt::NoButton, Qt::NoModifier, Qt::MouseEventNotSynthesized);
mouseEvent.setTimestamp(10);
QVERIFY(QCoreApplication::sendEvent(&window, &mouseEvent));
}
// Enter
{
QMouseEvent mouseEvent(QEvent::MouseMove, QPointF(50, 50), QPointF(50, 50), QPointF(150, 150),
Qt::NoButton, Qt::NoButton, Qt::NoModifier, Qt::MouseEventNotSynthesized);
mouseEvent.setTimestamp(20);
QVERIFY(QCoreApplication::sendEvent(&window, &mouseEvent));
}
QCOMPARE(hoverConsumer->hoverTimestamps.size(), 1);
QCOMPARE(hoverConsumer->hoverTimestamps.last(), 20UL);
// Move
{
QMouseEvent mouseEvent(QEvent::MouseMove, QPointF(60, 60), QPointF(60, 60), QPointF(160, 160),
Qt::NoButton, Qt::NoButton, Qt::NoModifier, Qt::MouseEventNotSynthesized);
mouseEvent.setTimestamp(30);
QVERIFY(QCoreApplication::sendEvent(&window, &mouseEvent));
}
QCOMPARE(hoverConsumer->hoverTimestamps.size(), 2);
QCOMPARE(hoverConsumer->hoverTimestamps.last(), 30UL);
// Move
{
QMouseEvent mouseEvent(QEvent::MouseMove, QPointF(100, 100), QPointF(100, 100), QPointF(200, 200),
Qt::NoButton, Qt::NoButton, Qt::NoModifier, Qt::MouseEventNotSynthesized);
mouseEvent.setTimestamp(40);
QVERIFY(QCoreApplication::sendEvent(&window, &mouseEvent));
}
QCOMPARE(hoverConsumer->hoverTimestamps.size(), 3);
QCOMPARE(hoverConsumer->hoverTimestamps.last(), 40UL);
// Leave
{
QMouseEvent mouseEvent(QEvent::MouseMove, QPointF(160, 160), QPointF(160, 160), QPointF(260, 260),
Qt::NoButton, Qt::NoButton, Qt::NoModifier, Qt::MouseEventNotSynthesized);
mouseEvent.setTimestamp(5);
QVERIFY(QCoreApplication::sendEvent(&window, &mouseEvent));
}
QCOMPARE(hoverConsumer->hoverTimestamps.size(), 4);
QCOMPARE(hoverConsumer->hoverTimestamps.last(), 5UL);
}
class CircleItem : public QQuickRectangle
{
public:
CircleItem(QQuickItem *parent = nullptr) : QQuickRectangle(parent) { }
void setRadius(qreal radius) {
const qreal diameter = radius*2;
setWidth(diameter);
setHeight(diameter);
}
bool childMouseEventFilter(QQuickItem *item, QEvent *event) override
{
Q_UNUSED(item)
if (event->type() == QEvent::MouseButtonPress && !contains(static_cast<QMouseEvent*>(event)->pos())) {
// This is an evil hack: in case of items that are not rectangles, we never accept the event.
// Instead the events are now delivered to QDeclarativeGeoMapItemBase which doesn't to anything with them.
// The map below it still works since it filters events and steals the events at some point.
event->setAccepted(false);
return true;
}
return false;
}
bool contains(const QPointF &pos) const override {
// returns true if the point is inside the the embedded circle inside the (square) rect
const float radius = (float)width()/2;
const QVector2D center(radius, radius);
const QVector2D dx = QVector2D(pos) - center;
const bool ret = dx.lengthSquared() < radius*radius;
return ret;
}
};
void tst_qquickwindow::test_circleMapItem()
{
QQuickWindow window;
window.resize(250, 250);
window.setPosition(100, 100);
window.setTitle(QTest::currentTestFunction());
QQuickItem *root = window.contentItem();
QQuickMouseArea *mab = new QQuickMouseArea(root);
mab->setObjectName("Bottom MouseArea");
mab->setSize(QSizeF(100, 100));
CircleItem *topItem = new CircleItem(root);
topItem->setFiltersChildMouseEvents(true);
topItem->setColor(Qt::green);
topItem->setObjectName("Top Item");
topItem->setPosition(QPointF(30, 30));
topItem->setRadius(20);
QQuickMouseArea *mat = new QQuickMouseArea(topItem);
mat->setObjectName("Top Item/MouseArea");
mat->setSize(QSizeF(40, 40));
QSignalSpy bottomSpy(mab, SIGNAL(clicked(QQuickMouseEvent *)));
QSignalSpy topSpy(mat, SIGNAL(clicked(QQuickMouseEvent *)));
window.show();
QVERIFY(QTest::qWaitForWindowExposed(&window));
QTest::qWait(1000);
QPoint pos(50, 50);
QTest::mouseClick(&window, Qt::LeftButton, Qt::KeyboardModifiers(), pos);
QCOMPARE(topSpy.count(), 1);
QCOMPARE(bottomSpy.count(), 0);
// Outside the "Circles" "input area", but on top of the bottomItem rectangle
pos = QPoint(66, 66);
QTest::mouseClick(&window, Qt::LeftButton, Qt::KeyboardModifiers(), pos);
QCOMPARE(bottomSpy.count(), 1);
QCOMPARE(topSpy.count(), 1);
}
void tst_qquickwindow::pointerEventTypeAndPointCount()
{
QPointF localPosition(33, 66);
QPointF scenePosition(133, 166);
QPointF screenPosition(333, 366);
QMouseEvent me(QEvent::MouseButtonPress, localPosition, scenePosition, screenPosition,
Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
QTouchEvent te(QEvent::TouchBegin, touchDevice, Qt::NoModifier, Qt::TouchPointPressed,
QList<QTouchEvent::TouchPoint>() << QTouchEvent::TouchPoint(1));
QQuickPointerMouseEvent pme(nullptr, QQuickPointerDevice::genericMouseDevice());
pme.reset(&me);
QCOMPARE(pme.asMouseEvent(localPosition), &me);
QVERIFY(pme.asPointerMouseEvent());
QVERIFY(!pme.asPointerTouchEvent());
QVERIFY(!pme.asPointerTabletEvent());
// QVERIFY(!pe->asTabletEvent()); // TODO
QCOMPARE(pme.pointCount(), 1);
QCOMPARE(pme.point(0)->scenePosition(), scenePosition);
QCOMPARE(pme.asMouseEvent(localPosition)->localPos(), localPosition);
QCOMPARE(pme.asMouseEvent(localPosition)->screenPos(), screenPosition);
QQuickPointerTouchEvent pte(nullptr, QQuickPointerDevice::touchDevice(touchDevice));
pte.reset(&te);
QCOMPARE(pte.asTouchEvent(), &te);
QVERIFY(!pte.asPointerMouseEvent());
QVERIFY(pte.asPointerTouchEvent());
QVERIFY(!pte.asPointerTabletEvent());
QVERIFY(pte.asTouchEvent());
// QVERIFY(!pte.asTabletEvent()); // TODO
QCOMPARE(pte.pointCount(), 1);
QCOMPARE(pte.touchPointById(1)->id(), 1);
QVERIFY(!pte.touchPointById(0));
te.setTouchPoints(QList<QTouchEvent::TouchPoint>() << QTouchEvent::TouchPoint(1) << QTouchEvent::TouchPoint(2));
pte.reset(&te);
QCOMPARE(pte.pointCount(), 2);
QCOMPARE(pte.touchPointById(1)->id(), 1);
QCOMPARE(pte.touchPointById(2)->id(), 2);
QVERIFY(!pte.touchPointById(0));
te.setTouchPoints(QList<QTouchEvent::TouchPoint>() << QTouchEvent::TouchPoint(2));
pte.reset(&te);
QCOMPARE(pte.pointCount(), 1);
QCOMPARE(pte.touchPointById(2)->id(), 2);
QVERIFY(!pte.touchPointById(1));
QVERIFY(!pte.touchPointById(0));
}
void tst_qquickwindow::grabContentItemToImage()
{
QQmlEngine engine;
QQmlComponent component(&engine);
component.loadUrl(testFileUrl("grabContentItemToImage.qml"));
QObject *created = component.create();
QScopedPointer<QObject> cleanup(created);
QVERIFY(created);
QQuickWindow *window = qobject_cast<QQuickWindow *>(created);
QVERIFY(QTest::qWaitForWindowActive(window));
QMetaObject::invokeMethod(window, "grabContentItemToImage");
QTRY_COMPARE(created->property("success").toInt(), 1);
}
class TestDropTarget : public QQuickItem
{
Q_OBJECT
public:
TestDropTarget(QQuickItem *parent = nullptr)
: QQuickItem(parent)
, enterDropAction(Qt::CopyAction)
, moveDropAction(Qt::CopyAction)
, dropDropAction(Qt::CopyAction)
, enterAccept(true)
, moveAccept(true)
, dropAccept(true)
{
setFlags(ItemAcceptsDrops);
}
void reset()
{
enterDropAction = Qt::CopyAction;
moveDropAction = Qt::CopyAction;
dropDropAction = Qt::CopyAction;
enterAccept = true;
moveAccept = true;
dropAccept = true;
}
void dragEnterEvent(QDragEnterEvent *event)
{
event->setAccepted(enterAccept);
event->setDropAction(enterDropAction);
}
void dragMoveEvent(QDragMoveEvent *event)
{
event->setAccepted(moveAccept);
event->setDropAction(moveDropAction);
}
void dropEvent(QDropEvent *event)
{
event->setAccepted(dropAccept);
event->setDropAction(dropDropAction);
}
Qt::DropAction enterDropAction;
Qt::DropAction moveDropAction;
Qt::DropAction dropDropAction;
bool enterAccept;
bool moveAccept;
bool dropAccept;
};
class DragEventTester {
public:
DragEventTester()
: pos(60, 60)
, actions(Qt::CopyAction | Qt::MoveAction | Qt::LinkAction)
, buttons(Qt::LeftButton)
, modifiers(Qt::NoModifier)
{
}
~DragEventTester() {
qDeleteAll(events);
events.clear();
enterEvent = nullptr;
moveEvent = nullptr;
dropEvent = nullptr;
leaveEvent = nullptr;
}
void addEnterEvent()
{
enterEvent = new QDragEnterEvent(pos, actions, &data, buttons, modifiers);
events.append(enterEvent);
}
void addMoveEvent()
{
moveEvent = new QDragMoveEvent(pos, actions, &data, buttons, modifiers, QEvent::DragMove);
events.append(moveEvent);
}
void addDropEvent()
{
dropEvent = new QDropEvent(pos, actions, &data, buttons, modifiers, QEvent::Drop);
events.append(dropEvent);
}
void addLeaveEvent()
{
leaveEvent = new QDragLeaveEvent();
events.append(leaveEvent);
}
void sendDragEventSequence(QQuickWindow *window) const {
for (int i = 0; i < events.size(); ++i) {
QCoreApplication::sendEvent(window, events[i]);
}
}
// Used for building events.
QMimeData data;
QPoint pos;
Qt::DropActions actions;
Qt::MouseButtons buttons;
Qt::KeyboardModifiers modifiers;
// Owns events.
QList<QEvent *> events;
// Non-owner pointers for easy acccess.
QDragEnterEvent *enterEvent;
QDragMoveEvent *moveEvent;
QDropEvent *dropEvent;
QDragLeaveEvent *leaveEvent;
};
void tst_qquickwindow::testDragEventPropertyPropagation()
{
QQuickWindow window;
TestDropTarget dropTarget(window.contentItem());
// Setting the size is important because the QQuickWindow checks if the drag happened inside
// the drop target.
dropTarget.setSize(QSizeF(100, 100));
// Test enter events property propagation.
// For enter events, only isAccepted gets propagated.
{
DragEventTester builder;
dropTarget.enterAccept = false;
dropTarget.enterDropAction = Qt::IgnoreAction;
builder.addEnterEvent(); builder.addMoveEvent(); builder.addLeaveEvent();
builder.sendDragEventSequence(&window);
QDragEnterEvent* enterEvent = builder.enterEvent;
QCOMPARE(enterEvent->isAccepted(), dropTarget.enterAccept);
}
{
DragEventTester builder;
dropTarget.enterAccept = false;
dropTarget.enterDropAction = Qt::CopyAction;
builder.addEnterEvent(); builder.addMoveEvent(); builder.addLeaveEvent();
builder.sendDragEventSequence(&window);
QDragEnterEvent* enterEvent = builder.enterEvent;
QCOMPARE(enterEvent->isAccepted(), dropTarget.enterAccept);
}
{
DragEventTester builder;
dropTarget.enterAccept = true;
dropTarget.enterDropAction = Qt::IgnoreAction;
builder.addEnterEvent(); builder.addMoveEvent(); builder.addLeaveEvent();
builder.sendDragEventSequence(&window);
QDragEnterEvent* enterEvent = builder.enterEvent;
QCOMPARE(enterEvent->isAccepted(), dropTarget.enterAccept);
}
{
DragEventTester builder;
dropTarget.enterAccept = true;
dropTarget.enterDropAction = Qt::CopyAction;
builder.addEnterEvent(); builder.addMoveEvent(); builder.addLeaveEvent();
builder.sendDragEventSequence(&window);
QDragEnterEvent* enterEvent = builder.enterEvent;
QCOMPARE(enterEvent->isAccepted(), dropTarget.enterAccept);
}
// Test move events property propagation.
// For move events, both isAccepted and dropAction get propagated.
dropTarget.reset();
{
DragEventTester builder;
dropTarget.moveAccept = false;
dropTarget.moveDropAction = Qt::IgnoreAction;
builder.addEnterEvent(); builder.addMoveEvent(); builder.addLeaveEvent();
builder.sendDragEventSequence(&window);
QDragMoveEvent* moveEvent = builder.moveEvent;
QCOMPARE(moveEvent->isAccepted(), dropTarget.moveAccept);
QCOMPARE(moveEvent->dropAction(), dropTarget.moveDropAction);
}
{
DragEventTester builder;
dropTarget.moveAccept = false;
dropTarget.moveDropAction = Qt::CopyAction;
builder.addEnterEvent(); builder.addMoveEvent(); builder.addLeaveEvent();
builder.sendDragEventSequence(&window);
QDragMoveEvent* moveEvent = builder.moveEvent;
QCOMPARE(moveEvent->isAccepted(), dropTarget.moveAccept);
QCOMPARE(moveEvent->dropAction(), dropTarget.moveDropAction);
}
{
DragEventTester builder;
dropTarget.moveAccept = true;
dropTarget.moveDropAction = Qt::IgnoreAction;
builder.addEnterEvent(); builder.addMoveEvent(); builder.addLeaveEvent();
builder.sendDragEventSequence(&window);
QDragMoveEvent* moveEvent = builder.moveEvent;
QCOMPARE(moveEvent->isAccepted(), dropTarget.moveAccept);
QCOMPARE(moveEvent->dropAction(), dropTarget.moveDropAction);
}
{
DragEventTester builder;
dropTarget.moveAccept = true;
dropTarget.moveDropAction = Qt::CopyAction;
builder.addEnterEvent(); builder.addMoveEvent(); builder.addLeaveEvent();
builder.sendDragEventSequence(&window);
QDragMoveEvent* moveEvent = builder.moveEvent;
QCOMPARE(moveEvent->isAccepted(), dropTarget.moveAccept);
QCOMPARE(moveEvent->dropAction(), dropTarget.moveDropAction);
}
// Test drop events property propagation.
// For drop events, both isAccepted and dropAction get propagated.
dropTarget.reset();
{
DragEventTester builder;
dropTarget.dropAccept = false;
dropTarget.dropDropAction = Qt::IgnoreAction;
builder.addEnterEvent(); builder.addMoveEvent(); builder.addDropEvent();
builder.sendDragEventSequence(&window);
QDropEvent* dropEvent = builder.dropEvent;
QCOMPARE(dropEvent->isAccepted(), dropTarget.dropAccept);
QCOMPARE(dropEvent->dropAction(), dropTarget.dropDropAction);
}
{
DragEventTester builder;
dropTarget.dropAccept = false;
dropTarget.dropDropAction = Qt::CopyAction;
builder.addEnterEvent(); builder.addMoveEvent(); builder.addDropEvent();
builder.sendDragEventSequence(&window);
QDropEvent* dropEvent = builder.dropEvent;
QCOMPARE(dropEvent->isAccepted(), dropTarget.dropAccept);
QCOMPARE(dropEvent->dropAction(), dropTarget.dropDropAction);
}
{
DragEventTester builder;
dropTarget.dropAccept = true;
dropTarget.dropDropAction = Qt::IgnoreAction;
builder.addEnterEvent(); builder.addMoveEvent(); builder.addDropEvent();
builder.sendDragEventSequence(&window);
QDropEvent* dropEvent = builder.dropEvent;
QCOMPARE(dropEvent->isAccepted(), dropTarget.dropAccept);
QCOMPARE(dropEvent->dropAction(), dropTarget.dropDropAction);
}
{
DragEventTester builder;
dropTarget.dropAccept = true;
dropTarget.dropDropAction = Qt::CopyAction;
builder.addEnterEvent(); builder.addMoveEvent(); builder.addDropEvent();
builder.sendDragEventSequence(&window);
QDropEvent* dropEvent = builder.dropEvent;
QCOMPARE(dropEvent->isAccepted(), dropTarget.dropAccept);
QCOMPARE(dropEvent->dropAction(), dropTarget.dropDropAction);
}
}
void tst_qquickwindow::findChild()
{
QQuickWindow window;
// QQuickWindow
// |_ QQuickWindow::contentItem
// | |_ QObject("contentItemChild")
// |_ QObject("viewChild")
QObject *windowChild = new QObject(&window);
windowChild->setObjectName("windowChild");
QObject *contentItemChild = new QObject(window.contentItem());
contentItemChild->setObjectName("contentItemChild");
QCOMPARE(window.findChild<QObject *>("windowChild"), windowChild);
QCOMPARE(window.findChild<QObject *>("contentItemChild"), contentItemChild);
QVERIFY(!window.contentItem()->findChild<QObject *>("viewChild")); // sibling
QCOMPARE(window.contentItem()->findChild<QObject *>("contentItemChild"), contentItemChild);
}
class DeliveryRecord : public QPair<QString, QString>
{
public:
DeliveryRecord(const QString &filter, const QString &receiver) : QPair(filter, receiver) { }
DeliveryRecord(const QString &receiver) : QPair(QString(), receiver) { }
DeliveryRecord() : QPair() { }
QString toString() const {
if (second.isEmpty())
return QLatin1String("Delivery(no receiver)");
else if (first.isEmpty())
return QString(QLatin1String("Delivery(to '%1')")).arg(second);
else
return QString(QLatin1String("Delivery('%1' filtering for '%2')")).arg(first).arg(second);
}
};
Q_DECLARE_METATYPE(DeliveryRecord)
QDebug operator<<(QDebug dbg, const DeliveryRecord &pair)
{
dbg << pair.toString();
return dbg;
}
typedef QVector<DeliveryRecord> DeliveryRecordVector;
class EventItem : public QQuickRectangle
{
Q_OBJECT
public:
EventItem(QQuickItem *parent)
: QQuickRectangle(parent)
, m_eventAccepts(true)
, m_filterReturns(true)
, m_filterAccepts(true)
, m_filterNotPreAccepted(false)
{
QSizeF psize(parent->width(), parent->height());
psize -= QSizeF(20, 20);
setWidth(psize.width());
setHeight(psize.height());
setPosition(QPointF(10, 10));
}
void setFilterReturns(bool filterReturns) { m_filterReturns = filterReturns; }
void setFilterAccepts(bool accepts) { m_filterAccepts = accepts; }
void setEventAccepts(bool accepts) { m_eventAccepts = accepts; }
/*!
* \internal
*
* returns false if any of the calls to childMouseEventFilter had the wrong
* preconditions. If all calls had the expected precondition, returns true.
*/
bool testFilterPreConditions() const { return !m_filterNotPreAccepted; }
static QVector<DeliveryRecord> &deliveryList() { return m_deliveryList; }
static QSet<QEvent::Type> &includedEventTypes()
{
if (m_includedEventTypes.isEmpty())
m_includedEventTypes << QEvent::MouseButtonPress;
return m_includedEventTypes;
}
static void setExpectedDeliveryList(const QVector<DeliveryRecord> &v) { m_expectedDeliveryList = v; }
protected:
bool childMouseEventFilter(QQuickItem *i, QEvent *e) override
{
appendEvent(this, i, e);
switch (e->type()) {
case QEvent::MouseButtonPress:
if (!e->isAccepted())
m_filterNotPreAccepted = true;
e->setAccepted(m_filterAccepts);
// qCDebug(lcTests) << objectName() << i->objectName();
return m_filterReturns;
default:
break;
}
return QQuickRectangle::childMouseEventFilter(i, e);
}
bool event(QEvent *e) override
{
appendEvent(nullptr, this, e);
switch (e->type()) {
case QEvent::MouseButtonPress:
// qCDebug(lcTests) << objectName();
e->setAccepted(m_eventAccepts);
return true;
default:
break;
}
return QQuickRectangle::event(e);
}
private:
static void appendEvent(QQuickItem *filter, QQuickItem *receiver, QEvent *event) {
if (includedEventTypes().contains(event->type())) {
auto record = DeliveryRecord(filter ? filter->objectName() : QString(), receiver ? receiver->objectName() : QString());
int i = m_deliveryList.count();
if (m_expectedDeliveryList.count() > i && m_expectedDeliveryList[i] == record)
qCDebug(lcTests).noquote().nospace() << i << ": " << record;
else
qCDebug(lcTests).noquote().nospace() << i << ": " << record
<< ", expected " << (m_expectedDeliveryList.count() > i ? m_expectedDeliveryList[i].toString() : QLatin1String("nothing")) << " <---";
m_deliveryList << record;
}
}
bool m_eventAccepts;
bool m_filterReturns;
bool m_filterAccepts;
bool m_filterNotPreAccepted;
// list of (filtering-parent . receiver) pairs
static DeliveryRecordVector m_expectedDeliveryList;
static DeliveryRecordVector m_deliveryList;
static QSet<QEvent::Type> m_includedEventTypes;
};
DeliveryRecordVector EventItem::m_expectedDeliveryList;
DeliveryRecordVector EventItem::m_deliveryList;
QSet<QEvent::Type> EventItem::m_includedEventTypes;
typedef QVector<const char*> CharStarVector;
Q_DECLARE_METATYPE(CharStarVector)
struct InputState {
struct {
// event() behavior
bool eventAccepts;
// filterChildMouse behavior
bool returns;
bool accepts;
bool filtersChildMouseEvent;
} r[4];
};
Q_DECLARE_METATYPE(InputState)
void tst_qquickwindow::testChildMouseEventFilter_data()
{
// HIERARCHY:
// r0->r1->r2->r3
//
QTest::addColumn<QPoint>("mousePos");
QTest::addColumn<InputState>("inputState");
QTest::addColumn<DeliveryRecordVector>("expectedDeliveryOrder");
QTest::newRow("if filtered and rejected, do not deliver it to the item that filtered it")
<< QPoint(100, 100)
<< InputState({
// | event() | child mouse filter
// +---------+---------+---------+---------
{ // | accepts | returns | accepts | filtersChildMouseEvent
{ false, false, false, false},
{ true, false, false, false},
{ false, true, false, true},
{ false, false, false, false}
}
})
<< (DeliveryRecordVector()
<< DeliveryRecord("r2", "r3")
//<< DeliveryRecord("r3") // it got filtered -> do not deliver
// DeliveryRecord("r2") // r2 filtered it -> do not deliver
<< DeliveryRecord("r1")
);
QTest::newRow("no filtering, no accepting")
<< QPoint(100, 100)
<< InputState({
// | event() | child mouse filter
// +---------+---------+---------+---------
{ // | accepts | returns | accepts | filtersChildMouseEvent
{ false, false, false, false},
{ false , false, false, false},
{ false, false, false, false},
{ false, false, false, false}
}
})
<< (DeliveryRecordVector()
<< DeliveryRecord("r3")
<< DeliveryRecord("r2")
<< DeliveryRecord("r1")
<< DeliveryRecord("r0")
<< DeliveryRecord("root")
);
QTest::newRow("all filtering, no accepting")
<< QPoint(100, 100)
<< InputState({
// | event() | child mouse filter
// +---------+---------+---------+---------
{ // | accepts | returns | accepts | filtersChildMouseEvent
{ false, false, false, true},
{ false, false, false, true},
{ false, false, false, true},
{ false, false, false, true}
}
})
<< (DeliveryRecordVector()
<< DeliveryRecord("r2", "r3")
<< DeliveryRecord("r1", "r3")
<< DeliveryRecord("r0", "r3")
<< DeliveryRecord("r3")
<< DeliveryRecord("r1", "r2")
<< DeliveryRecord("r0", "r2")
<< DeliveryRecord("r2")
<< DeliveryRecord("r0", "r1")
<< DeliveryRecord("r1")
<< DeliveryRecord("r0")
<< DeliveryRecord("root")
);
QTest::newRow("some filtering, no accepting")
<< QPoint(100, 100)
<< InputState({
// | event() | child mouse filter
// +---------+---------+---------+---------
{ // | accepts | returns | accepts | filtersChildMouseEvent
{ false, false, false, true},
{ false, false, false, true},
{ false, false, false, false},
{ false, false, false, false}
}
})
<< (DeliveryRecordVector()
<< DeliveryRecord("r1", "r3")
<< DeliveryRecord("r0", "r3")
<< DeliveryRecord("r3")
<< DeliveryRecord("r1", "r2")
<< DeliveryRecord("r0", "r2")
<< DeliveryRecord("r2")
<< DeliveryRecord("r0", "r1")
<< DeliveryRecord("r1")
<< DeliveryRecord("r0")
<< DeliveryRecord("root")
);
QTest::newRow("r1 accepts")
<< QPoint(100, 100)
<< InputState({
// | event() | child mouse filter
// +---------+---------+---------+---------
{ // | accepts | returns | accepts | filtersChildMouseEvent
{ false, false, false, true},
{ true , false, false, true},
{ false, false, false, false},
{ false, false, false, false}
}
})
<< (DeliveryRecordVector()
<< DeliveryRecord("r1", "r3")
<< DeliveryRecord("r0", "r3")
<< DeliveryRecord("r3")
<< DeliveryRecord("r1", "r2")
<< DeliveryRecord("r0", "r2")
<< DeliveryRecord("r2")
<< DeliveryRecord("r0", "r1")
<< DeliveryRecord("r1")
);
QTest::newRow("r1 rejects and filters")
<< QPoint(100, 100)
<< InputState({
// | event() | child mouse filter
// +---------+---------+---------+---------
{ // | accepts | returns | accepts | filtersChildMouseEvent
{ false, false, false, true},
{ false , true, false, true},
{ false, false, false, false},
{ false, false, false, false}
}
})
<< (DeliveryRecordVector()
<< DeliveryRecord("r1", "r3")
<< DeliveryRecord("r0", "r3")
// << DeliveryRecord("r3") // since it got filtered we don't deliver to r3
<< DeliveryRecord("r1", "r2")
<< DeliveryRecord("r0", "r2")
// << DeliveryRecord("r2" // since it got filtered we don't deliver to r2
<< DeliveryRecord("r0", "r1")
// << DeliveryRecord("r1") // since it acted as a filter and returned true, we don't deliver to r1
<< DeliveryRecord("r0")
<< DeliveryRecord("root")
);
}
void tst_qquickwindow::testChildMouseEventFilter()
{
QFETCH(QPoint, mousePos);
QFETCH(InputState, inputState);
QFETCH(DeliveryRecordVector, expectedDeliveryOrder);
EventItem::setExpectedDeliveryList(expectedDeliveryOrder);
QQuickWindow window;
window.resize(500, 809);
QQuickItem *root = window.contentItem();
root->setAcceptedMouseButtons(Qt::LeftButton);
root->setObjectName("root");
EventFilter *rootFilter = new EventFilter;
root->installEventFilter(rootFilter);
// Create 4 items; each item a child of the previous item.
EventItem *r[4];
r[0] = new EventItem(root);
r[0]->setColor(QColor(0x404040));
r[0]->setWidth(200);
r[0]->setHeight(200);
r[1] = new EventItem(r[0]);
r[1]->setColor(QColor(0x606060));
r[2] = new EventItem(r[1]);
r[2]->setColor(Qt::red);
r[3] = new EventItem(r[2]);
r[3]->setColor(Qt::green);
for (uint i = 0; i < sizeof(r)/sizeof(EventItem*); ++i) {
r[i]->setEventAccepts(inputState.r[i].eventAccepts);
r[i]->setFilterReturns(inputState.r[i].returns);
r[i]->setFilterAccepts(inputState.r[i].accepts);
r[i]->setFiltersChildMouseEvents(inputState.r[i].filtersChildMouseEvent);
r[i]->setObjectName(QString::fromLatin1("r%1").arg(i));
r[i]->setAcceptedMouseButtons(Qt::LeftButton);
}
window.show();
window.requestActivate();
QVERIFY(QTest::qWaitForWindowActive(&window));
DeliveryRecordVector &actualDeliveryOrder = EventItem::deliveryList();
actualDeliveryOrder.clear();
QTest::mousePress(&window, Qt::LeftButton, Qt::NoModifier, mousePos);
// Check if event got delivered to the root item. If so, append it to the list of items the event got delivered to
if (rootFilter->events.contains(QEvent::MouseButtonPress))
actualDeliveryOrder.append(DeliveryRecord("root"));
for (int i = 0; i < qMax(actualDeliveryOrder.count(), expectedDeliveryOrder.count()); ++i) {
const DeliveryRecord expectedNames = expectedDeliveryOrder.value(i);
const DeliveryRecord actualNames = actualDeliveryOrder.value(i);
QCOMPARE(actualNames.toString(), expectedNames.toString());
}
for (EventItem *item : r) {
QVERIFY(item->testFilterPreConditions());
}
// "restore" mouse state
QTest::mouseRelease(&window, Qt::LeftButton, Qt::NoModifier, mousePos);
}
void tst_qquickwindow::cleanupGrabsOnRelease()
{
TestTouchItem::clearMouseEventCounters();
QQuickWindow *window = new QQuickWindow;
QScopedPointer<QQuickWindow> cleanup(window);
window->resize(250, 250);
window->setPosition(100, 100);
window->setTitle(QTest::currentTestFunction());
window->show();
QVERIFY(QTest::qWaitForWindowActive(window));
TestTouchItem *parent = new TestTouchItem(window->contentItem());
parent->setObjectName("parent");
parent->setSize(QSizeF(150, 150));
parent->acceptMouseEvents = true;
parent->grabOnRelease = true;
TestTouchItem *child = new TestTouchItem(parent);
child->setObjectName("child");
child->setSize(QSizeF(100, 100));
child->acceptMouseEvents = true;
QPoint pos(80, 80);
QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, pos);
QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, pos);
// There is an explicit parent->grabMouse on release(!). This means grab changes from child
// to parent:
// This will emit two ungrab events:
// 1. One for the child (due to the explicit call to parent->grabMouse())
// 2. One for the parent (since the mouse button was finally released)
QCOMPARE(child->mouseUngrabEventCount, 1);
QCOMPARE(parent->mouseUngrabEventCount, 1);
}
#if QT_CONFIG(shortcut)
void tst_qquickwindow::testShortCut()
{
QQmlEngine engine;
QQmlComponent component(&engine);
component.loadUrl(testFileUrl("shortcut.qml"));
QObject *created = component.create();
QScopedPointer<QObject> cleanup(created);
QVERIFY(created);
QQuickWindow *window = qobject_cast<QQuickWindow *>(created);
QVERIFY(QTest::qWaitForWindowActive(window));
EventFilter eventFilter;
window->activeFocusItem()->installEventFilter(&eventFilter);
//Send non-spontaneous key press event
QKeyEvent keyEvent(QEvent::KeyPress, Qt::Key_B, Qt::NoModifier);
QCoreApplication::sendEvent(window, &keyEvent);
QVERIFY(eventFilter.events.contains(int(QEvent::ShortcutOverride)));
QVERIFY(window->property("received").value<bool>());
}
#endif
QTEST_MAIN(tst_qquickwindow)
#include "tst_qquickwindow.moc"