// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only

#include <QtTest/QtTest>
#include <QtTest/QSignalSpy>
#include <QtQuick/qquickitem.h>
#include <QtQuick/qquickview.h>
#include <QtQml/qqmlcontext.h>
#include <QtQml/qqmlengine.h>
#include <QtQml/qqmlexpression.h>
#include <QQmlComponent>

template <typename T> static T evaluate(QObject *scope, const QString &expression)
{
    QQmlExpression expr(qmlContext(scope), scope, expression);
    QVariant result = expr.evaluate();
    if (expr.hasError())
        qWarning() << expr.error().toString();
    return result.value<T>();
}

template <> void evaluate<void>(QObject *scope, const QString &expression)
{
    QQmlExpression expr(qmlContext(scope), scope, expression);
    expr.evaluate();
    if (expr.hasError())
        qWarning() << expr.error().toString();
}

Q_DECLARE_METATYPE(Qt::DropActions)

class TestDropTarget : public QQuickItem
{
    Q_OBJECT
public:
    TestDropTarget(QQuickItem *parent = nullptr)
        : QQuickItem(parent)
    {
        setFlags(ItemAcceptsDrops);
    }

    void reset()
    {
        enterEvents = 0;
        moveEvents = 0;
        leaveEvents = 0;
        dropEvents = 0;
        defaultAction = Qt::IgnoreAction;
        proposedAction = Qt::IgnoreAction;
        supportedActions = Qt::IgnoreAction;
    }

    void dragEnterEvent(QDragEnterEvent *event) override
    {
        ++enterEvents;
        position = event->position().toPoint();
        defaultAction = event->dropAction();
        proposedAction = event->proposedAction();
        supportedActions = event->possibleActions();
        event->setAccepted(accept);
    }

    void dragMoveEvent(QDragMoveEvent *event) override
    {
        ++moveEvents;
        position = event->position().toPoint();
        defaultAction = event->dropAction();
        proposedAction = event->proposedAction();
        supportedActions = event->possibleActions();
        event->setAccepted(accept);
    }

    void dragLeaveEvent(QDragLeaveEvent *event) override
    {
        ++leaveEvents;
        event->setAccepted(accept);
    }

    void dropEvent(QDropEvent *event) override
    {
        ++dropEvents;
        position = event->position().toPoint();
        defaultAction = event->dropAction();
        proposedAction = event->proposedAction();
        supportedActions = event->possibleActions();
        event->setDropAction(acceptAction);
        event->setAccepted(accept);
    }

    int enterEvents = 0;
    int moveEvents = 0;
    int leaveEvents = 0;
    int dropEvents = 0;
    Qt::DropAction acceptAction = Qt::MoveAction;
    Qt::DropAction defaultAction = Qt::IgnoreAction;
    Qt::DropAction proposedAction = Qt::IgnoreAction;
    Qt::DropActions supportedActions;
    QPointF position;
    bool accept = true;
};

class tst_QQuickDrag: public QObject
{
    Q_OBJECT
private slots:
    void initTestCase();
    void cleanupTestCase();

    void active();
    void setActive_data();
    void setActive();
    void drop();
    void move();
    void parentChange();
    void hotSpot();
    void supportedActions();
    void proposedAction();
    void keys();
    void source();
    void recursion_data();
    void recursion();
    void noCrashWithImageProvider();

private:
    QQmlEngine engine;
};

void tst_QQuickDrag::initTestCase()
{

}

void tst_QQuickDrag::cleanupTestCase()
{

}

void tst_QQuickDrag::active()
{
    QQuickWindow window;
    TestDropTarget dropTarget(window.contentItem());
    dropTarget.setSize(QSizeF(100, 100));
    QQmlComponent component(&engine);
    component.setData(
            "import QtQuick 2.0\n"
            "Item {\n"
                "property bool dragActive: Drag.active\n"
                "property Item dragTarget: Drag.target\n"
                "x: 50; y: 50\n"
                "width: 10; height: 10\n"
            "}", QUrl());
    QScopedPointer<QObject> object(component.create());
    QQuickItem *item = qobject_cast<QQuickItem *>(object.data());
    QVERIFY(item);
    item->setParentItem(&dropTarget);

    QCOMPARE(evaluate<bool>(item, "Drag.active"), false);
    QCOMPARE(evaluate<bool>(item, "dragActive"), false);

    evaluate<void>(item, "Drag.active = true");
    QCOMPARE(evaluate<bool>(item, "Drag.active"), true);
    QCOMPARE(evaluate<bool>(item, "dragActive"), true);
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(&dropTarget));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(&dropTarget));
    QCOMPARE(dropTarget.enterEvents, 1); QCOMPARE(dropTarget.leaveEvents, 0);

    dropTarget.reset();
    evaluate<void>(item, "Drag.active = false");
    QCOMPARE(evaluate<bool>(item, "Drag.active"), false);
    QCOMPARE(evaluate<bool>(item, "dragActive"), false);
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(nullptr));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(nullptr));
    QCOMPARE(dropTarget.enterEvents, 0); QCOMPARE(dropTarget.leaveEvents, 1);

    dropTarget.reset();
    evaluate<void>(item, "Drag.cancel()");
    QCOMPARE(evaluate<bool>(item, "Drag.active"), false);
    QCOMPARE(evaluate<bool>(item, "dragActive"), false);
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(nullptr));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(nullptr));
    QCOMPARE(dropTarget.enterEvents, 0); QCOMPARE(dropTarget.leaveEvents, 0);

    dropTarget.reset();
    evaluate<void>(item, "Drag.start()");
    QCOMPARE(evaluate<bool>(item, "Drag.active"), true);
    QCOMPARE(evaluate<bool>(item, "dragActive"), true);
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(&dropTarget));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(&dropTarget));
    QCOMPARE(dropTarget.enterEvents, 1); QCOMPARE(dropTarget.leaveEvents, 0);

    // Start while a drag is active, cancels the previous drag and starts a new one.
    dropTarget.reset();
    evaluate<void>(item, "Drag.start()");
    QCOMPARE(evaluate<bool>(item, "Drag.active"), true);
    QCOMPARE(evaluate<bool>(item, "dragActive"), true);
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(&dropTarget));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(&dropTarget));
    QCOMPARE(dropTarget.enterEvents, 1); QCOMPARE(dropTarget.leaveEvents, 1);

    dropTarget.reset();
    evaluate<void>(item, "Drag.cancel()");
    QCOMPARE(evaluate<bool>(item, "Drag.active"), false);
    QCOMPARE(evaluate<bool>(item, "dragActive"), false);
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(nullptr));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(nullptr));
    QCOMPARE(dropTarget.enterEvents, 0); QCOMPARE(dropTarget.leaveEvents, 1);

    // Enter events aren't sent to items without the QQuickItem::ItemAcceptsDrops flag.
    dropTarget.setFlags(QQuickItem::Flags());

    dropTarget.reset();
    evaluate<void>(item, "Drag.active = true");
    QCOMPARE(evaluate<bool>(item, "Drag.active"), true);
    QCOMPARE(evaluate<bool>(item, "dragActive"), true);
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(nullptr));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(nullptr));
    QCOMPARE(dropTarget.enterEvents, 0); QCOMPARE(dropTarget.leaveEvents, 0);

    dropTarget.reset();
    evaluate<void>(item, "Drag.active = false");
    QCOMPARE(evaluate<bool>(item, "Drag.active"), false);
    QCOMPARE(evaluate<bool>(item, "dragActive"), false);
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(nullptr));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(nullptr));
    QCOMPARE(dropTarget.enterEvents, 0); QCOMPARE(dropTarget.leaveEvents, 0);

    dropTarget.setFlags(QQuickItem::ItemAcceptsDrops);

    dropTarget.reset();
    evaluate<void>(item, "Drag.active = true");
    QCOMPARE(evaluate<bool>(item, "Drag.active"), true);
    QCOMPARE(evaluate<bool>(item, "dragActive"), true);
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(&dropTarget));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(&dropTarget));
    QCOMPARE(dropTarget.enterEvents, 1); QCOMPARE(dropTarget.leaveEvents, 0);

    dropTarget.setFlags(QQuickItem::Flags());

    dropTarget.reset();
    evaluate<void>(item, "Drag.active = false");
    QCOMPARE(evaluate<bool>(item, "Drag.active"), false);
    QCOMPARE(evaluate<bool>(item, "dragActive"), false);
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(nullptr));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(nullptr));
    QCOMPARE(dropTarget.enterEvents, 0); QCOMPARE(dropTarget.leaveEvents, 1);

    // Follow up events aren't sent to items if the enter event isn't accepted.
    dropTarget.setFlags(QQuickItem::ItemAcceptsDrops);
    dropTarget.accept = false;

    dropTarget.reset();
    evaluate<void>(item, "Drag.active = true");
    QCOMPARE(evaluate<bool>(item, "Drag.active"), true);
    QCOMPARE(evaluate<bool>(item, "dragActive"), true);
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(nullptr));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(nullptr));
    QCOMPARE(dropTarget.enterEvents, 1); QCOMPARE(dropTarget.leaveEvents, 0);

    dropTarget.reset();
    evaluate<void>(item, "Drag.active = false");
    QCOMPARE(evaluate<bool>(item, "Drag.active"), false);
    QCOMPARE(evaluate<bool>(item, "dragActive"), false);
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(nullptr));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(nullptr));
    QCOMPARE(dropTarget.enterEvents, 0); QCOMPARE(dropTarget.leaveEvents, 0);

    dropTarget.accept = true;

    dropTarget.reset();
    evaluate<void>(item, "Drag.active = true");
    QCOMPARE(evaluate<bool>(item, "Drag.active"), true);
    QCOMPARE(evaluate<bool>(item, "dragActive"), true);
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(&dropTarget));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(&dropTarget));
    QCOMPARE(dropTarget.enterEvents, 1); QCOMPARE(dropTarget.leaveEvents, 0);

    dropTarget.accept = false;

    dropTarget.reset();
    evaluate<void>(item, "Drag.active = false");
    QCOMPARE(evaluate<bool>(item, "Drag.active"), false);
    QCOMPARE(evaluate<bool>(item, "dragActive"), false);
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(nullptr));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(nullptr));
    QCOMPARE(dropTarget.enterEvents, 0); QCOMPARE(dropTarget.leaveEvents, 1);

    // Events are sent to hidden or disabled items.
    dropTarget.accept = true;
    dropTarget.setVisible(false);
    dropTarget.reset();
    evaluate<void>(item, "Drag.active = true");
    QCOMPARE(evaluate<bool>(item, "Drag.active"), true);
    QCOMPARE(evaluate<bool>(item, "dragActive"), true);
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(nullptr));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(nullptr));
    QCOMPARE(dropTarget.enterEvents, 0); QCOMPARE(dropTarget.leaveEvents, 0);

    evaluate<void>(item, "Drag.active = false");
    dropTarget.setVisible(true);

    dropTarget.setOpacity(0.0);
    dropTarget.reset();
    evaluate<void>(item, "Drag.active = true");
    QCOMPARE(evaluate<bool>(item, "Drag.active"), true);
    QCOMPARE(evaluate<bool>(item, "dragActive"), true);
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(&dropTarget));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(&dropTarget));
    QCOMPARE(dropTarget.enterEvents, 1); QCOMPARE(dropTarget.leaveEvents, 0);

    evaluate<void>(item, "Drag.active = false");
    dropTarget.setOpacity(1.0);

    dropTarget.setEnabled(false);
    dropTarget.reset();
    evaluate<void>(item, "Drag.active = true");
    QCOMPARE(evaluate<bool>(item, "Drag.active"), true);
    QCOMPARE(evaluate<bool>(item, "dragActive"), true);
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(nullptr));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(nullptr));
    QCOMPARE(dropTarget.enterEvents, 0); QCOMPARE(dropTarget.leaveEvents, 0);

    evaluate<void>(item, "Drag.active = false");
    dropTarget.setEnabled(true);
    dropTarget.reset();

    // Queued move events are discarded if the drag is cancelled.
    evaluate<void>(item, "Drag.active = true");
    QCOMPARE(evaluate<bool>(item, "Drag.active"), true);
    QCOMPARE(evaluate<bool>(item, "dragActive"), true);
    QCOMPARE(dropTarget.enterEvents, 1); QCOMPARE(dropTarget.leaveEvents, 0); QCOMPARE(dropTarget.moveEvents, 0);

    dropTarget.reset();
    item->setPosition(QPointF(80, 80));
    QCOMPARE(evaluate<bool>(item, "Drag.active"), true);
    QCOMPARE(evaluate<bool>(item, "dragActive"), true);
    QCOMPARE(dropTarget.enterEvents, 0); QCOMPARE(dropTarget.leaveEvents, 0); QCOMPARE(dropTarget.moveEvents, 0);

    evaluate<void>(item, "Drag.active = false");
    QCOMPARE(evaluate<bool>(item, "Drag.active"), false);
    QCOMPARE(evaluate<bool>(item, "dragActive"), false);
    QCOMPARE(dropTarget.enterEvents, 0); QCOMPARE(dropTarget.leaveEvents, 1); QCOMPARE(dropTarget.moveEvents, 0);

    dropTarget.reset();
    QCoreApplication::processEvents();
    QCOMPARE(dropTarget.enterEvents, 0); QCOMPARE(dropTarget.leaveEvents, 0); QCOMPARE(dropTarget.moveEvents, 0);
}

void tst_QQuickDrag::setActive_data()
{
    QTest::addColumn<QString>("dragType");

    QTest::newRow("default") << "";
    QTest::newRow("internal") << "Drag.dragType: Drag.Internal";
    QTest::newRow("none") << "Drag.dragType: Drag.None";
    /* We don't test Drag.Automatic, because that causes QDrag::exec() to be
     * invoked, and on some platforms tha's implemented by running a main loop
     * until the drag has finished -- and at that point, the Drag.active will
     * be false again. */
}

// QTBUG-52540
void tst_QQuickDrag::setActive()
{
    QFETCH(QString, dragType);

    QQuickWindow window;
    TestDropTarget dropTarget(window.contentItem());
    dropTarget.setSize(QSizeF(100, 100));
    QQmlComponent component(&engine);
    component.setData(
            "import QtQuick 2.0\n"
            "Item {\n"
                "property bool dragActive: Drag.active\n"
                "property Item dragTarget: Drag.target\n" +
                dragType.toUtf8() + "\n"
                "x: 50; y: 50\n"
                "width: 10; height: 10\n"
            "}", QUrl());
    QScopedPointer<QObject> object(component.create());
    QQuickItem *item = qobject_cast<QQuickItem *>(object.data());
    QVERIFY(item);
    item->setParentItem(&dropTarget);

    QCOMPARE(evaluate<bool>(item, "Drag.active"), false);
    QCOMPARE(evaluate<bool>(item, "dragActive"), false);

    evaluate<void>(item, "Drag.active = true");
    QCOMPARE(evaluate<bool>(item, "Drag.active"), true);
    QCOMPARE(evaluate<bool>(item, "dragActive"), true);
}

void tst_QQuickDrag::drop()
{
    QQuickWindow window;
    TestDropTarget outerTarget(window.contentItem());
    outerTarget.setSize(QSizeF(100, 100));
    outerTarget.acceptAction = Qt::CopyAction;
    TestDropTarget innerTarget(&outerTarget);
    innerTarget.setSize(QSizeF(100, 100));
    innerTarget.acceptAction = Qt::MoveAction;
    QQmlComponent component(&engine);
    component.setData(
            "import QtQuick 2.0\n"
            "Item {\n"
                "property bool dragActive: Drag.active\n"
                "property Item dragTarget: Drag.target\n"
                "x: 50; y: 50\n"
                "width: 10; height: 10\n"
            "}", QUrl());
    QScopedPointer<QObject> object(component.create());
    QQuickItem *item = qobject_cast<QQuickItem *>(object.data());
    QVERIFY(item);
    item->setParentItem(&outerTarget);

    innerTarget.reset(); outerTarget.reset();
    evaluate<void>(item, "Drag.active = true");
    QCOMPARE(evaluate<bool>(item, "Drag.active"), true);
    QCOMPARE(evaluate<bool>(item, "dragActive"), true);
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(&innerTarget));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(&innerTarget));
    QCOMPARE(outerTarget.enterEvents, 0); QCOMPARE(outerTarget.leaveEvents, 0); QCOMPARE(outerTarget.dropEvents, 0);
    QCOMPARE(innerTarget.enterEvents, 1); QCOMPARE(innerTarget.leaveEvents, 0); QCOMPARE(innerTarget.dropEvents, 0);

    innerTarget.reset(); outerTarget.reset();
    QCOMPARE(evaluate<bool>(item, "Drag.drop() == Qt.MoveAction"), true);
    QCOMPARE(evaluate<bool>(item, "Drag.active"), false);
    QCOMPARE(evaluate<bool>(item, "dragActive"), false);
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(&innerTarget));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(&innerTarget));
    QCOMPARE(outerTarget.enterEvents, 0); QCOMPARE(outerTarget.leaveEvents, 0); QCOMPARE(outerTarget.dropEvents, 0);
    QCOMPARE(innerTarget.enterEvents, 0); QCOMPARE(innerTarget.leaveEvents, 0); QCOMPARE(innerTarget.dropEvents, 1);

    innerTarget.reset(); outerTarget.reset();
    evaluate<void>(item, "Drag.active = true");
    QCOMPARE(evaluate<bool>(item, "Drag.active"), true);
    QCOMPARE(evaluate<bool>(item, "dragActive"), true);
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(&innerTarget));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(&innerTarget));
    QCOMPARE(outerTarget.enterEvents, 0); QCOMPARE(outerTarget.leaveEvents, 0); QCOMPARE(outerTarget.dropEvents, 0);
    QCOMPARE(innerTarget.enterEvents, 1); QCOMPARE(innerTarget.leaveEvents, 0); QCOMPARE(innerTarget.dropEvents, 0);

    evaluate<void>(item, "Drag.active = false");

    // Inner target doesn't accept enter so drop goes directly to outer.
    innerTarget.accept = false;
    innerTarget.setFlags(QQuickItem::Flags());

    innerTarget.reset(); outerTarget.reset();
    evaluate<void>(item, "Drag.active = true");
    QCOMPARE(evaluate<bool>(item, "Drag.active"), true);
    QCOMPARE(evaluate<bool>(item, "dragActive"), true);
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(&outerTarget));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(&outerTarget));
    QCOMPARE(outerTarget.enterEvents, 1); QCOMPARE(outerTarget.leaveEvents, 0); QCOMPARE(outerTarget.dropEvents, 0);
    QCOMPARE(innerTarget.enterEvents, 0); QCOMPARE(innerTarget.leaveEvents, 0); QCOMPARE(innerTarget.dropEvents, 0);

    innerTarget.reset(); outerTarget.reset();
    QCOMPARE(evaluate<bool>(item, "Drag.drop() == Qt.CopyAction"), true);
    QCOMPARE(evaluate<bool>(item, "Drag.active"), false);
    QCOMPARE(evaluate<bool>(item, "dragActive"), false);
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(&outerTarget));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(&outerTarget));
    QCOMPARE(outerTarget.enterEvents, 0); QCOMPARE(outerTarget.leaveEvents, 0); QCOMPARE(outerTarget.dropEvents, 1);
    QCOMPARE(innerTarget.enterEvents, 0); QCOMPARE(innerTarget.leaveEvents, 0); QCOMPARE(innerTarget.dropEvents, 0);

    // Neither target accepts drop so Qt::IgnoreAction is returned.
    innerTarget.reset(); outerTarget.reset();
    evaluate<void>(item, "Drag.active = true");
    QCOMPARE(evaluate<bool>(item, "Drag.active"), true);
    QCOMPARE(evaluate<bool>(item, "dragActive"), true);
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(&outerTarget));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(&outerTarget));
    QCOMPARE(outerTarget.enterEvents, 1); QCOMPARE(outerTarget.leaveEvents, 0); QCOMPARE(outerTarget.dropEvents, 0);
    QCOMPARE(innerTarget.enterEvents, 0); QCOMPARE(innerTarget.leaveEvents, 0); QCOMPARE(innerTarget.dropEvents, 0);

    outerTarget.accept = false;

    innerTarget.reset(); outerTarget.reset();
    QCOMPARE(evaluate<bool>(item, "Drag.drop() == Qt.IgnoreAction"), true);
    QCOMPARE(evaluate<bool>(item, "Drag.active"), false);
    QCOMPARE(evaluate<bool>(item, "dragActive"), false);
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(nullptr));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(nullptr));
    QCOMPARE(outerTarget.enterEvents, 0); QCOMPARE(outerTarget.leaveEvents, 0); QCOMPARE(outerTarget.dropEvents, 1);
    QCOMPARE(innerTarget.enterEvents, 0); QCOMPARE(innerTarget.leaveEvents, 0); QCOMPARE(innerTarget.dropEvents, 0);

    // drop doesn't send an event and returns Qt.IgnoreAction if not active.
    innerTarget.accept = true;
    outerTarget.accept = true;
    innerTarget.reset(); outerTarget.reset();
    QCOMPARE(evaluate<bool>(item, "Drag.drop() == Qt.IgnoreAction"), true);
    QCOMPARE(evaluate<bool>(item, "Drag.active"), false);
    QCOMPARE(evaluate<bool>(item, "dragActive"), false);
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(nullptr));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(nullptr));
    QCOMPARE(outerTarget.enterEvents, 0); QCOMPARE(outerTarget.leaveEvents, 0); QCOMPARE(outerTarget.dropEvents, 0);
    QCOMPARE(innerTarget.enterEvents, 0); QCOMPARE(innerTarget.leaveEvents, 0); QCOMPARE(innerTarget.dropEvents, 0);

    // Queued move event is delivered before a drop event.
    innerTarget.reset(); outerTarget.reset();
    evaluate<void>(item, "Drag.active = true");
    item->setPosition(QPointF(80, 80));
    evaluate<void>(item, "Drag.drop()");
    QCOMPARE(evaluate<bool>(item, "Drag.active"), false);
    QCOMPARE(evaluate<bool>(item, "dragActive"), false);
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(&outerTarget));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(&outerTarget));
    QCOMPARE(outerTarget.enterEvents, 1); QCOMPARE(outerTarget.leaveEvents, 0); QCOMPARE(outerTarget.dropEvents, 1); QCOMPARE(outerTarget.moveEvents, 1);
    QCOMPARE(innerTarget.enterEvents, 0); QCOMPARE(innerTarget.leaveEvents, 0); QCOMPARE(innerTarget.dropEvents, 0); QCOMPARE(innerTarget.moveEvents, 0);

    innerTarget.reset(); outerTarget.reset();
    QCoreApplication::processEvents();
    QCOMPARE(outerTarget.enterEvents, 0); QCOMPARE(outerTarget.leaveEvents, 0); QCOMPARE(outerTarget.dropEvents, 0); QCOMPARE(outerTarget.moveEvents, 0);
    QCOMPARE(innerTarget.enterEvents, 0); QCOMPARE(innerTarget.leaveEvents, 0); QCOMPARE(innerTarget.dropEvents, 0); QCOMPARE(innerTarget.moveEvents, 0);
}

void tst_QQuickDrag::move()
{
    QQuickWindow window;
    TestDropTarget outerTarget(window.contentItem());
    outerTarget.setSize(QSizeF(100, 100));
    TestDropTarget leftTarget(&outerTarget);
    leftTarget.setPosition(QPointF(0, 35));
    leftTarget.setSize(QSizeF(30, 30));
    TestDropTarget rightTarget(&outerTarget);
    rightTarget.setPosition(QPointF(70, 35));
    rightTarget.setSize(QSizeF(30, 30));
    QQmlComponent component(&engine);
    component.setData(
            "import QtQuick 2.0\n"
            "Item {\n"
                "property bool dragActive: Drag.active\n"
                "property Item dragTarget: Drag.target\n"
                "x: 50; y: 50\n"
                "width: 10; height: 10\n"
            "}", QUrl());
    QScopedPointer<QObject> object(component.create());
    QQuickItem *item = qobject_cast<QQuickItem *>(object.data());
    QVERIFY(item);
    item->setParentItem(&outerTarget);

    evaluate<void>(item, "Drag.active = true");
    QCOMPARE(evaluate<bool>(item, "Drag.active"), true);
    QCOMPARE(evaluate<bool>(item, "dragActive"), true);
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(&outerTarget));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(&outerTarget));
    QCOMPARE(outerTarget.enterEvents, 1); QCOMPARE(outerTarget.leaveEvents, 0); QCOMPARE(outerTarget.moveEvents, 0);
    QCOMPARE(leftTarget .enterEvents, 0); QCOMPARE(leftTarget .leaveEvents, 0); QCOMPARE(leftTarget .moveEvents, 0);
    QCOMPARE(rightTarget.enterEvents, 0); QCOMPARE(rightTarget.leaveEvents, 0); QCOMPARE(rightTarget.moveEvents, 0);
    QCOMPARE(outerTarget.position.x(), qreal(50)); QCOMPARE(outerTarget.position.y(), qreal(50));

    // Move within the outer target.
    outerTarget.reset(); leftTarget.reset(); rightTarget.reset();
    item->setPosition(QPointF(60, 50));
    // Move event is delivered in the event loop.
    QCOMPARE(outerTarget.enterEvents, 0); QCOMPARE(outerTarget.leaveEvents, 0); QCOMPARE(outerTarget.moveEvents, 0);
    QCOMPARE(leftTarget .enterEvents, 0); QCOMPARE(leftTarget .leaveEvents, 0); QCOMPARE(leftTarget .moveEvents, 0);
    QCOMPARE(rightTarget.enterEvents, 0); QCOMPARE(rightTarget.leaveEvents, 0); QCOMPARE(rightTarget.moveEvents, 0);
    QCoreApplication::processEvents();
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(&outerTarget));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(&outerTarget));
    QCOMPARE(outerTarget.enterEvents, 0); QCOMPARE(outerTarget.leaveEvents, 0); QCOMPARE(outerTarget.moveEvents, 1);
    QCOMPARE(leftTarget .enterEvents, 0); QCOMPARE(leftTarget .leaveEvents, 0); QCOMPARE(leftTarget .moveEvents, 0);
    QCOMPARE(rightTarget.enterEvents, 0); QCOMPARE(rightTarget.leaveEvents, 0); QCOMPARE(rightTarget.moveEvents, 0);
    QCOMPARE(outerTarget.position.x(), qreal(60)); QCOMPARE(outerTarget.position.y(), qreal(50));

    // Move into the right target.
    outerTarget.reset(); leftTarget.reset(); rightTarget.reset();
    // Setting X and Y individually should still only generate on move.
    item->setX(75);
    item->setY(50);
    QCOMPARE(outerTarget.enterEvents, 0); QCOMPARE(outerTarget.leaveEvents, 0); QCOMPARE(outerTarget.moveEvents, 0);
    QCOMPARE(leftTarget .enterEvents, 0); QCOMPARE(leftTarget .leaveEvents, 0); QCOMPARE(leftTarget .moveEvents, 0);
    QCOMPARE(rightTarget.enterEvents, 0); QCOMPARE(rightTarget.leaveEvents, 0); QCOMPARE(rightTarget.moveEvents, 0);
    QCoreApplication::processEvents();
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(&rightTarget));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(&rightTarget));
    QCOMPARE(outerTarget.enterEvents, 0); QCOMPARE(outerTarget.leaveEvents, 1); QCOMPARE(outerTarget.moveEvents, 0);
    QCOMPARE(leftTarget .enterEvents, 0); QCOMPARE(leftTarget .leaveEvents, 0); QCOMPARE(leftTarget .moveEvents, 0);
    QCOMPARE(rightTarget.enterEvents, 1); QCOMPARE(rightTarget.leaveEvents, 0); QCOMPARE(rightTarget.moveEvents, 0);
    QCOMPARE(rightTarget.position.x(), qreal(5)); QCOMPARE(rightTarget.position.y(), qreal(15));

    // Move into the left target.
    outerTarget.reset(); leftTarget.reset(); rightTarget.reset();
    item->setPosition(QPointF(25, 50));
    QCoreApplication::processEvents();
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(&leftTarget));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(&leftTarget));
    QCOMPARE(outerTarget.enterEvents, 0); QCOMPARE(outerTarget.leaveEvents, 0); QCOMPARE(outerTarget.moveEvents, 0);
    QCOMPARE(leftTarget .enterEvents, 1); QCOMPARE(leftTarget .leaveEvents, 0); QCOMPARE(leftTarget .moveEvents, 0);
    QCOMPARE(rightTarget.enterEvents, 0); QCOMPARE(rightTarget.leaveEvents, 1); QCOMPARE(rightTarget.moveEvents, 0);
    QCOMPARE(leftTarget.position.x(), qreal(25)); QCOMPARE(leftTarget.position.y(), qreal(15));

    // Move within the left target.
    outerTarget.reset(); leftTarget.reset(); rightTarget.reset();
    item->setPosition(QPointF(25, 40));
    QCoreApplication::processEvents();
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(&leftTarget));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(&leftTarget));
    QCOMPARE(outerTarget.enterEvents, 0); QCOMPARE(outerTarget.leaveEvents, 0); QCOMPARE(outerTarget.moveEvents, 0);
    QCOMPARE(leftTarget .enterEvents, 0); QCOMPARE(leftTarget .leaveEvents, 0); QCOMPARE(leftTarget .moveEvents, 1);
    QCOMPARE(rightTarget.enterEvents, 0); QCOMPARE(rightTarget.leaveEvents, 0); QCOMPARE(rightTarget.moveEvents, 0);
    QCOMPARE(outerTarget.position.x(), qreal(60)); QCOMPARE(outerTarget.position.y(), qreal(50));
    QCOMPARE(leftTarget.position.x(), qreal(25)); QCOMPARE(leftTarget.position.y(), qreal(5));

    // Move out of all targets.
    outerTarget.reset(); leftTarget.reset(); rightTarget.reset();
    item->setPosition(QPointF(110, 50));
    QCoreApplication::processEvents();
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(nullptr));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(nullptr));
    QCOMPARE(outerTarget.enterEvents, 0); QCOMPARE(outerTarget.leaveEvents, 0); QCOMPARE(outerTarget.moveEvents, 0);
    QCOMPARE(leftTarget .enterEvents, 0); QCOMPARE(leftTarget .leaveEvents, 1); QCOMPARE(leftTarget .moveEvents, 0);
    QCOMPARE(rightTarget.enterEvents, 0); QCOMPARE(rightTarget.leaveEvents, 0); QCOMPARE(rightTarget.moveEvents, 0);

    // Stop the right target accepting drag events and move into it.
    rightTarget.accept = false;

    outerTarget.reset(); leftTarget.reset(); rightTarget.reset();
    item->setPosition(QPointF(80, 50));
    QCoreApplication::processEvents();
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(&outerTarget));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(&outerTarget));
    QCOMPARE(outerTarget.enterEvents, 1); QCOMPARE(outerTarget.leaveEvents, 0); QCOMPARE(outerTarget.moveEvents, 0);
    QCOMPARE(leftTarget .enterEvents, 0); QCOMPARE(leftTarget .leaveEvents, 0); QCOMPARE(leftTarget .moveEvents, 0);
    QCOMPARE(rightTarget.enterEvents, 1); QCOMPARE(rightTarget.leaveEvents, 0); QCOMPARE(rightTarget.moveEvents, 0);
    QCOMPARE(outerTarget.position.x(), qreal(80)); QCOMPARE(outerTarget.position.y(), qreal(50));

    // Stop the outer target accepting drag events after it has accepted an enter event.
    outerTarget.accept = false;

    outerTarget.reset(); leftTarget.reset(); rightTarget.reset();
    item->setPosition(QPointF(60, 50));
    QCoreApplication::processEvents();
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(&outerTarget));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(&outerTarget));
    QCOMPARE(outerTarget.enterEvents, 0); QCOMPARE(outerTarget.leaveEvents, 0); QCOMPARE(outerTarget.moveEvents, 1);
    QCOMPARE(leftTarget .enterEvents, 0); QCOMPARE(leftTarget .leaveEvents, 0); QCOMPARE(leftTarget .moveEvents, 0);
    QCOMPARE(rightTarget.enterEvents, 0); QCOMPARE(rightTarget.leaveEvents, 0); QCOMPARE(rightTarget.moveEvents, 0);
    QCOMPARE(outerTarget.position.x(), qreal(60)); QCOMPARE(outerTarget.position.y(), qreal(50));

    // Clear the QQuickItem::ItemAcceptsDrops flag from the outer target after it accepted an enter event.
    outerTarget.setFlags(QQuickItem::Flags());

    outerTarget.reset(); leftTarget.reset(); rightTarget.reset();
    item->setPosition(QPointF(40, 50));
    QCoreApplication::processEvents();
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(&outerTarget));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(&outerTarget));
    QCOMPARE(outerTarget.enterEvents, 0); QCOMPARE(outerTarget.leaveEvents, 0); QCOMPARE(outerTarget.moveEvents, 1);
    QCOMPARE(leftTarget .enterEvents, 0); QCOMPARE(leftTarget .leaveEvents, 0); QCOMPARE(leftTarget .moveEvents, 0);
    QCOMPARE(rightTarget.enterEvents, 0); QCOMPARE(rightTarget.leaveEvents, 0); QCOMPARE(rightTarget.moveEvents, 0);
    QCOMPARE(outerTarget.position.x(), qreal(40)); QCOMPARE(outerTarget.position.y(), qreal(50));

    // Clear the QQuickItem::ItemAcceptsDrops flag from the left target before it accepts an enter event.
    leftTarget.setFlags(QQuickItem::Flags());

    outerTarget.reset(); leftTarget.reset(); rightTarget.reset();
    item->setPosition(QPointF(25, 50));
    QCoreApplication::processEvents();
    QCOMPARE(evaluate<QObject *>(item, "Drag.target"), static_cast<QObject *>(&outerTarget));
    QCOMPARE(evaluate<QObject *>(item, "dragTarget"), static_cast<QObject *>(&outerTarget));
    QCOMPARE(outerTarget.enterEvents, 0); QCOMPARE(outerTarget.leaveEvents, 0); QCOMPARE(outerTarget.moveEvents, 1);
    QCOMPARE(leftTarget .enterEvents, 0); QCOMPARE(leftTarget .leaveEvents, 0); QCOMPARE(leftTarget .moveEvents, 0);
    QCOMPARE(rightTarget.enterEvents, 0); QCOMPARE(rightTarget.leaveEvents, 0); QCOMPARE(rightTarget.moveEvents, 0);
    QCOMPARE(outerTarget.position.x(), qreal(25)); QCOMPARE(outerTarget.position.y(), qreal(50));
}

void tst_QQuickDrag::parentChange()
{
    QQuickWindow window1;
    TestDropTarget dropTarget1(window1.contentItem());
    dropTarget1.setSize(QSizeF(100, 100));

    QQuickWindow window2;
    TestDropTarget dropTarget2(window2.contentItem());
    dropTarget2.setSize(QSizeF(100, 100));

    QQmlComponent component(&engine);
    component.setData(
            "import QtQuick 2.0\n"
            "Item {\n"
                "property real hotSpotX: Drag.hotSpot.x\n"
                "property real hotSpotY: Drag.hotSpot.y\n"
                "x: 50; y: 50\n"
                "width: 10; height: 10\n"
                "Drag.active: true\n"
            "}", QUrl());
    QScopedPointer<QObject> object(component.create());
    QQuickItem *item = qobject_cast<QQuickItem *>(object.data());
    QVERIFY(item);

    QCOMPARE(evaluate<bool>(item, "Drag.active"), true);

    // Verify setting a parent item for an item with an active drag sends an enter event.
    item->setParentItem(window1.contentItem());
    QCOMPARE(dropTarget1.enterEvents, 0);
    QCoreApplication::processEvents();
    QCOMPARE(dropTarget1.enterEvents, 1);

    // Changing the parent within the same window should send a move event.
    item->setParentItem(&dropTarget1);
    QCOMPARE(dropTarget1.enterEvents, 1);
    QCOMPARE(dropTarget1.moveEvents, 0);
    QCoreApplication::processEvents();
    QCOMPARE(dropTarget1.enterEvents, 1);
    QCOMPARE(dropTarget1.moveEvents, 1);

    // Changing the parent to an item in another window sends a leave event in the old window
    // and an enter on the new window.
    item->setParentItem(window2.contentItem());
    QCOMPARE(dropTarget1.enterEvents, 1);
    QCOMPARE(dropTarget1.moveEvents, 1);
    QCOMPARE(dropTarget1.leaveEvents, 0);
    QCOMPARE(dropTarget2.enterEvents, 0);
    QCoreApplication::processEvents();
    QCOMPARE(dropTarget1.enterEvents, 1);
    QCOMPARE(dropTarget1.moveEvents, 1);
    QCOMPARE(dropTarget1.leaveEvents, 1);
    QCOMPARE(dropTarget2.enterEvents, 1);

    // Removing then parent item sends a leave event.
    item->setParentItem(nullptr);
    QCOMPARE(dropTarget1.enterEvents, 1);
    QCOMPARE(dropTarget1.moveEvents, 1);
    QCOMPARE(dropTarget1.leaveEvents, 1);
    QCOMPARE(dropTarget2.enterEvents, 1);
    QCOMPARE(dropTarget2.leaveEvents, 0);
    QCoreApplication::processEvents();
    QCOMPARE(dropTarget1.enterEvents, 1);
    QCOMPARE(dropTarget1.moveEvents, 1);
    QCOMPARE(dropTarget1.leaveEvents, 1);
    QCOMPARE(dropTarget2.enterEvents, 1);
    QCOMPARE(dropTarget2.leaveEvents, 1);

    // Go around again and verify no events if active is false.
    evaluate<void>(item, "Drag.active = false");
    item->setParentItem(window1.contentItem());
    QCoreApplication::processEvents();

    item->setParentItem(&dropTarget1);
    QCoreApplication::processEvents();

    item->setParentItem(window2.contentItem());
    QCoreApplication::processEvents();

    item->setParentItem(nullptr);
    QCoreApplication::processEvents();
    QCOMPARE(dropTarget1.enterEvents, 1);
    QCOMPARE(dropTarget1.moveEvents, 1);
    QCOMPARE(dropTarget1.leaveEvents, 1);
    QCOMPARE(dropTarget2.enterEvents, 1);
    QCOMPARE(dropTarget2.leaveEvents, 1);
}

void tst_QQuickDrag::hotSpot()
{
    QQuickWindow window;
    TestDropTarget dropTarget(window.contentItem());
    dropTarget.setSize(QSizeF(100, 100));
    QQmlComponent component(&engine);
    component.setData(
            "import QtQuick 2.0\n"
            "Item {\n"
                "property real hotSpotX: Drag.hotSpot.x\n"
                "property real hotSpotY: Drag.hotSpot.y\n"
                "x: 50; y: 50\n"
                "width: 10; height: 10\n"
            "}", QUrl());
    QScopedPointer<QObject> object(component.create());
    QQuickItem *item = qobject_cast<QQuickItem *>(object.data());
    QVERIFY(item);
    item->setParentItem(&dropTarget);

    QCOMPARE(evaluate<qreal>(item, "Drag.hotSpot.x"), qreal(0));
    QCOMPARE(evaluate<qreal>(item, "Drag.hotSpot.y"), qreal(0));
    QCOMPARE(evaluate<qreal>(item, "hotSpotX"), qreal(0));
    QCOMPARE(evaluate<qreal>(item, "hotSpotY"), qreal(0));

    evaluate<void>(item, "{ Drag.start(); Drag.cancel() }");
    QCOMPARE(dropTarget.position.x(), qreal(50));
    QCOMPARE(dropTarget.position.y(), qreal(50));

    evaluate<void>(item, "{ Drag.hotSpot.x = 5, Drag.hotSpot.y = 5 }");
    QCOMPARE(evaluate<qreal>(item, "Drag.hotSpot.x"), qreal(5));
    QCOMPARE(evaluate<qreal>(item, "Drag.hotSpot.y"), qreal(5));
    QCOMPARE(evaluate<qreal>(item, "hotSpotX"), qreal(5));
    QCOMPARE(evaluate<qreal>(item, "hotSpotY"), qreal(5));

    evaluate<void>(item, "Drag.start()");
    QCOMPARE(dropTarget.position.x(), qreal(55));
    QCOMPARE(dropTarget.position.y(), qreal(55));

    item->setPosition(QPointF(30, 20));
    QCoreApplication::processEvents();
    QCOMPARE(dropTarget.position.x(), qreal(35));
    QCOMPARE(dropTarget.position.y(), qreal(25));

    evaluate<void>(item, "{ Drag.hotSpot.x = 10; Drag.hotSpot.y = 10 }");
    QCOMPARE(evaluate<qreal>(item, "Drag.hotSpot.x"), qreal(10));
    QCOMPARE(evaluate<qreal>(item, "Drag.hotSpot.y"), qreal(10));
    QCOMPARE(evaluate<qreal>(item, "hotSpotX"), qreal(10));
    QCOMPARE(evaluate<qreal>(item, "hotSpotY"), qreal(10));

    // Setting the hotSpot will deliver a move event in the event loop.
    QCOMPARE(dropTarget.position.x(), qreal(35));
    QCOMPARE(dropTarget.position.y(), qreal(25));
    QCoreApplication::processEvents();
    QCOMPARE(dropTarget.position.x(), qreal(40));
    QCOMPARE(dropTarget.position.y(), qreal(30));

    item->setPosition(QPointF(10, 20));
    QCoreApplication::processEvents();
    QCOMPARE(dropTarget.position.x(), qreal(20));
    QCOMPARE(dropTarget.position.y(), qreal(30));

    evaluate<void>(item, "{ Drag.hotSpot.x = 10; Drag.hotSpot.y = 10 }");
}

void tst_QQuickDrag::supportedActions()
{
    QQuickWindow window;
    TestDropTarget dropTarget(window.contentItem());
    dropTarget.setSize(QSizeF(100, 100));
    QQmlComponent component(&engine);
    component.setData(
            "import QtQuick 2.0\n"
            "Item {\n"
                "property int supportedActions: Drag.supportedActions\n"
                "x: 50; y: 50\n"
                "width: 10; height: 10\n"
            "}", QUrl());
    QScopedPointer<QObject> object(component.create());
    QQuickItem *item = qobject_cast<QQuickItem *>(object.data());
    QVERIFY(item);
    item->setParentItem(&dropTarget);

    QCOMPARE(evaluate<bool>(item, "Drag.supportedActions == Qt.CopyAction | Qt.MoveAction | Qt.LinkAction"), true);
    QCOMPARE(evaluate<bool>(item, "supportedActions == Qt.CopyAction | Qt.MoveAction | Qt.LinkAction"), true);
    evaluate<void>(item, "{ Drag.start(); Drag.cancel() }");
    QCOMPARE(dropTarget.supportedActions, Qt::CopyAction | Qt::MoveAction | Qt::LinkAction);

    dropTarget.reset();
    evaluate<void>(item, "Drag.supportedActions = Qt.CopyAction | Qt.MoveAction");
    QCOMPARE(evaluate<bool>(item, "Drag.supportedActions == Qt.CopyAction | Qt.MoveAction"), true);
    QCOMPARE(evaluate<bool>(item, "supportedActions == Qt.CopyAction | Qt.MoveAction"), true);
    evaluate<void>(item, "Drag.start()");
    QCOMPARE(dropTarget.supportedActions, Qt::CopyAction | Qt::MoveAction);
    QCOMPARE(dropTarget.leaveEvents, 0);
    QCOMPARE(dropTarget.enterEvents, 1);

    // Changing the supported actions will restart the drag, after a delay to avoid any
    // recursion.
    evaluate<void>(item, "Drag.supportedActions = Qt.MoveAction");
    QCOMPARE(evaluate<bool>(item, "Drag.supportedActions == Qt.MoveAction"), true);
    QCOMPARE(evaluate<bool>(item, "supportedActions == Qt.MoveAction"), true);
    item->setPosition(QPointF(60, 60));
    QCOMPARE(dropTarget.supportedActions, Qt::CopyAction | Qt::MoveAction);
    QCOMPARE(dropTarget.leaveEvents, 0);
    QCOMPARE(dropTarget.enterEvents, 1);
    QCoreApplication::processEvents();
    QCOMPARE(dropTarget.supportedActions, Qt::MoveAction);
    QCOMPARE(dropTarget.leaveEvents, 1);
    QCOMPARE(dropTarget.enterEvents, 2);

    // Calling start with proposed actions will override the current actions for the next sequence.
    evaluate<void>(item, "Drag.start(Qt.CopyAction)");
    QCOMPARE(evaluate<bool>(item, "Drag.supportedActions == Qt.MoveAction"), true);
    QCOMPARE(evaluate<bool>(item, "supportedActions == Qt.MoveAction"), true);
    QCOMPARE(dropTarget.supportedActions, Qt::CopyAction);

    evaluate<void>(item, "Drag.start()");
    QCOMPARE(evaluate<bool>(item, "Drag.supportedActions == Qt.MoveAction"), true);
    QCOMPARE(evaluate<bool>(item, "supportedActions == Qt.MoveAction"), true);
    QCOMPARE(dropTarget.supportedActions, Qt::MoveAction);
}

void tst_QQuickDrag::proposedAction()
{
    QQuickWindow window;
    TestDropTarget dropTarget(window.contentItem());
    dropTarget.setSize(QSizeF(100, 100));
    QQmlComponent component(&engine);
    component.setData(
            "import QtQuick 2.0\n"
            "Item {\n"
                "property int proposedAction: Drag.proposedAction\n"
                "x: 50; y: 50\n"
                "width: 10; height: 10\n"
            "}", QUrl());
    QScopedPointer<QObject> object(component.create());
    QQuickItem *item = qobject_cast<QQuickItem *>(object.data());
    QVERIFY(item);
    item->setParentItem(&dropTarget);

    QCOMPARE(evaluate<bool>(item, "Drag.proposedAction == Qt.MoveAction"), true);
    QCOMPARE(evaluate<bool>(item, "proposedAction == Qt.MoveAction"), true);
    evaluate<void>(item, "{ Drag.start(); Drag.cancel() }");
    QCOMPARE(dropTarget.defaultAction, Qt::MoveAction);
    QCOMPARE(dropTarget.proposedAction, Qt::MoveAction);

    evaluate<void>(item, "Drag.proposedAction = Qt.CopyAction");
    QCOMPARE(evaluate<bool>(item, "Drag.proposedAction == Qt.CopyAction"), true);
    QCOMPARE(evaluate<bool>(item, "proposedAction == Qt.CopyAction"), true);
    evaluate<void>(item, "Drag.start()");
    QCOMPARE(dropTarget.defaultAction, Qt::CopyAction);
    QCOMPARE(dropTarget.proposedAction, Qt::CopyAction);

    // The proposed action can change during a drag.
    evaluate<void>(item, "Drag.proposedAction = Qt.MoveAction");
    QCOMPARE(evaluate<bool>(item, "Drag.proposedAction == Qt.MoveAction"), true);
    QCOMPARE(evaluate<bool>(item, "proposedAction == Qt.MoveAction"), true);
    QCoreApplication::processEvents();
    QCOMPARE(dropTarget.defaultAction, Qt::MoveAction);
    QCOMPARE(dropTarget.proposedAction, Qt::MoveAction);

    evaluate<void>(item, "Drag.proposedAction = Qt.LinkAction");
    QCOMPARE(evaluate<bool>(item, "Drag.proposedAction == Qt.LinkAction"), true);
    QCOMPARE(evaluate<bool>(item, "proposedAction == Qt.LinkAction"), true);
    evaluate<void>(item, "Drag.drop()");
    QCOMPARE(dropTarget.defaultAction, Qt::LinkAction);
    QCOMPARE(dropTarget.proposedAction, Qt::LinkAction);
}

void tst_QQuickDrag::keys()
{
    QQmlComponent component(&engine);
    component.setData(
            "import QtQuick 2.0\n"
            "Item {\n"
                "property variant keys: Drag.keys\n"
                "x: 50; y: 50\n"
                "width: 10; height: 10\n"
            "}", QUrl());
    QScopedPointer<QObject> object(component.create());
    QQuickItem *item = qobject_cast<QQuickItem *>(object.data());
    QVERIFY(item);

    QCOMPARE(evaluate<QStringList>(item, "Drag.keys"), QStringList());
    QCOMPARE(evaluate<QStringList>(item, "keys"), QStringList());
    QCOMPARE(item->property("keys").toStringList(), QStringList());

    evaluate<void>(item, "Drag.keys = [\"red\", \"blue\"]");
    QCOMPARE(evaluate<QStringList>(item, "Drag.keys"), QStringList() << "red" << "blue");
    QCOMPARE(evaluate<QStringList>(item, "keys"), QStringList() << "red" << "blue");
    QCOMPARE(item->property("keys").toStringList(), QStringList() << "red" << "blue");

    // Test changing the keys restarts a drag.
    QQuickWindow window;
    item->setParentItem(window.contentItem());
    TestDropTarget dropTarget(window.contentItem());
    dropTarget.setSize(QSizeF(100, 100));

    evaluate<void>(item, "Drag.start()");
    QCOMPARE(dropTarget.leaveEvents, 0);
    QCOMPARE(dropTarget.enterEvents, 1);

    evaluate<void>(item, "Drag.keys = [\"green\"]");
    QCOMPARE(dropTarget.leaveEvents, 0);
    QCOMPARE(dropTarget.enterEvents, 1);
    QCoreApplication::processEvents();
    QCOMPARE(dropTarget.leaveEvents, 1);
    QCOMPARE(dropTarget.enterEvents, 2);
}

void tst_QQuickDrag::source()
{

    QQmlComponent component(&engine);
    component.setData(
            "import QtQuick 2.0\n"
            "Item {\n"
                "property Item source: Drag.source\n"
                "x: 50; y: 50\n"
                "width: 10; height: 10\n"
                "Item { id: proxySource; objectName: \"proxySource\" }\n"
            "}", QUrl());
    QScopedPointer<QObject> object(component.create());
    QQuickItem *item = qobject_cast<QQuickItem *>(object.data());
    QVERIFY(item);

    QCOMPARE(evaluate<QObject *>(item, "Drag.source"), static_cast<QObject *>(item));
    QCOMPARE(evaluate<QObject *>(item, "source"), static_cast<QObject *>(item));

    QQuickItem *proxySource = item->findChild<QQuickItem *>("proxySource");
    QVERIFY(proxySource);

    evaluate<void>(item, "Drag.source = proxySource");
    QCOMPARE(evaluate<QObject *>(item, "Drag.source"), static_cast<QObject *>(proxySource));
    QCOMPARE(evaluate<QObject *>(item, "source"), static_cast<QObject *>(proxySource));

    evaluate<void>(item, "Drag.source = undefined");
    QCOMPARE(evaluate<QObject *>(item, "Drag.source"), static_cast<QObject *>(item));
    QCOMPARE(evaluate<QObject *>(item, "source"), static_cast<QObject *>(item));

    // Test changing the source restarts a drag.
    QQuickWindow window;
    item->setParentItem(window.contentItem());
    TestDropTarget dropTarget(window.contentItem());
    dropTarget.setSize(QSizeF(100, 100));

    evaluate<void>(item, "Drag.start()");
    QCOMPARE(dropTarget.leaveEvents, 0);
    QCOMPARE(dropTarget.enterEvents, 1);

    evaluate<void>(item, "Drag.source = proxySource");
    QCOMPARE(dropTarget.leaveEvents, 0);
    QCOMPARE(dropTarget.enterEvents, 1);
    QCoreApplication::processEvents();
    QCOMPARE(dropTarget.leaveEvents, 1);
    QCOMPARE(dropTarget.enterEvents, 2);
}

class RecursingDropTarget : public TestDropTarget
{
public:
    RecursingDropTarget(const QString &script, int type, QQuickItem *parent)
        : TestDropTarget(parent), script(script), type(type), item(nullptr) {}

    void setItem(QQuickItem *i) { item = i; }

protected:
    void dragEnterEvent(QDragEnterEvent *event) override
    {
        TestDropTarget::dragEnterEvent(event);
        if (type == QEvent::DragEnter && enterEvents < 2)
            evaluate<void>(item, script);
    }

    void dragMoveEvent(QDragMoveEvent *event) override
    {
        TestDropTarget::dragMoveEvent(event);
        if (type == QEvent::DragMove && moveEvents < 2)
            evaluate<void>(item, script);
    }

    void dragLeaveEvent(QDragLeaveEvent *event) override
    {
        TestDropTarget::dragLeaveEvent(event);
        if (type == QEvent::DragLeave && leaveEvents < 2)
            evaluate<void>(item, script);
    }

    void dropEvent(QDropEvent *event) override
    {
        TestDropTarget::dropEvent(event);
        if (type == QEvent::Drop && dropEvents < 2)
            evaluate<void>(item, script);
    }

private:
    QString script;
    int type;
    QQuickItem *item;

};

void tst_QQuickDrag::recursion_data()
{
    QTest::addColumn<QString>("script");
    QTest::addColumn<int>("type");
    QTest::addColumn<int>("moveEvents");
    QTest::addColumn<QRegularExpression>("warning");

    QTest::newRow("Drag.start() in Enter")
            << QString("Drag.start()")
            << int(QEvent::DragEnter)
            << 1
            << QRegularExpression(".*start\\(\\) cannot be called from within a drag event handler");
    QTest::newRow("Drag.cancel() in Enter")
            << QString("Drag.cancel()")
            << int(QEvent::DragEnter)
            << 1
            << QRegularExpression(".*cancel\\(\\) cannot be called from within a drag event handler");
    QTest::newRow("Drag.drop() in Enter")
            << QString("Drag.drop()")
            << int(QEvent::DragEnter)
            << 1
            << QRegularExpression(".*drop\\(\\) cannot be called from within a drag event handler");
    QTest::newRow("Drag.active = true in Enter")
            << QString("Drag.active = true")
            << int(QEvent::DragEnter)
            << 1
            << QRegularExpression();
    QTest::newRow("Drag.active = false in Enter")
            << QString("Drag.active = false")
            << int(QEvent::DragEnter)
            << 1
            << QRegularExpression(".*active cannot be changed from within a drag event handler");
    QTest::newRow("move in Enter")
            << QString("x = 23")
            << int(QEvent::DragEnter)
            << 1
            << QRegularExpression();

    QTest::newRow("Drag.start() in Move")
            << QString("Drag.start()")
            << int(QEvent::DragMove)
            << 1
            << QRegularExpression(".*start\\(\\) cannot be called from within a drag event handler");
    QTest::newRow("Drag.cancel() in Move")
            << QString("Drag.cancel()")
            << int(QEvent::DragMove)
            << 1
            << QRegularExpression(".*cancel\\(\\) cannot be called from within a drag event handler");
    QTest::newRow("Drag.drop() in Move")
            << QString("Drag.drop()")
            << int(QEvent::DragMove)
            << 1
            << QRegularExpression(".*drop\\(\\) cannot be called from within a drag event handler");
    QTest::newRow("Drag.active = true in Move")
            << QString("Drag.active = true")
            << int(QEvent::DragMove)
            << 1
            << QRegularExpression();
    QTest::newRow("Drag.active = false in Move")
            << QString("Drag.active = false")
            << int(QEvent::DragMove)
            << 1
            << QRegularExpression(".*active cannot be changed from within a drag event handler");
    QTest::newRow("move in Move")
            << QString("x = 23")
            << int(QEvent::DragMove)
            << 2
            << QRegularExpression();

    QTest::newRow("Drag.start() in Leave")
            << QString("Drag.start()")
            << int(QEvent::DragLeave)
            << 1
            << QRegularExpression(".*start\\(\\) cannot be called from within a drag event handler");
    QTest::newRow("Drag.cancel() in Leave")
            << QString("Drag.cancel()")
            << int(QEvent::DragLeave)
            << 1
            << QRegularExpression(".*cancel\\(\\) cannot be called from within a drag event handler");
    QTest::newRow("Drag.drop() in Leave")
            << QString("Drag.drop()")
            << int(QEvent::DragLeave)
            << 1
            << QRegularExpression(".*drop\\(\\) cannot be called from within a drag event handler");
    QTest::newRow("Drag.active = true in Leave")
            << QString("Drag.active = true")
            << int(QEvent::DragLeave)
            << 1
            << QRegularExpression(".*active cannot be changed from within a drag event handler");
    QTest::newRow("Drag.active = false in Leave")
            << QString("Drag.active = false")
            << int(QEvent::DragLeave)
            << 1
            << QRegularExpression();
    QTest::newRow("move in Leave")
            << QString("x = 23")
            << int(QEvent::DragLeave)
            << 1
            << QRegularExpression();

    QTest::newRow("Drag.start() in Drop")
            << QString("Drag.start()")
            << int(QEvent::Drop)
            << 1
            << QRegularExpression(".*start\\(\\) cannot be called from within a drag event handler");
    QTest::newRow("Drag.cancel() in Drop")
            << QString("Drag.cancel()")
            << int(QEvent::Drop)
            << 1
            << QRegularExpression(".*cancel\\(\\) cannot be called from within a drag event handler");
    QTest::newRow("Drag.drop() in Drop")
            << QString("Drag.drop()")
            << int(QEvent::Drop)
            << 1
            << QRegularExpression(".*drop\\(\\) cannot be called from within a drag event handler");
    QTest::newRow("Drag.active = true in Drop")
            << QString("Drag.active = true")
            << int(QEvent::Drop)
            << 1
            << QRegularExpression(".*active cannot be changed from within a drag event handler");
    QTest::newRow("Drag.active = false in Drop")
            << QString("Drag.active = false")
            << int(QEvent::Drop)
            << 1
            << QRegularExpression();
    QTest::newRow("move in Drop")
            << QString("x = 23")
            << int(QEvent::Drop)
            << 1
            << QRegularExpression();
}

void tst_QQuickDrag::recursion()
{
    QFETCH(QString, script);
    QFETCH(int, type);
    QFETCH(int, moveEvents);
    QFETCH(QRegularExpression, warning);

    if (!warning.pattern().isEmpty())
        QTest::ignoreMessage(QtWarningMsg, warning);

    QQuickWindow window;
    RecursingDropTarget dropTarget(script, type, window.contentItem());
    dropTarget.setSize(QSizeF(100, 100));
    QQmlComponent component(&engine);
    component.setData(
            "import QtQuick 2.0\n"
            "Item {\n"
                "x: 50; y: 50\n"
                "width: 10; height: 10\n"
            "}", QUrl());
    QScopedPointer<QObject> object(component.create());
    QQuickItem *item = qobject_cast<QQuickItem *>(object.data());
    QVERIFY(item);
    item->setParentItem(window.contentItem());

    dropTarget.setItem(item);

    evaluate<void>(item, "Drag.start()");
    QCOMPARE(dropTarget.enterEvents, 1);
    QCOMPARE(dropTarget.moveEvents, 0);
    QCOMPARE(dropTarget.dropEvents, 0);
    QCOMPARE(dropTarget.leaveEvents, 0);

    evaluate<void>(item, "y = 15");

    // the evaluate statement above, y = 15, will cause
    // QQuickItem::setY(15) to be called, leading to an
    // event being posted that will be delivered
    // to RecursingDropTarget::dragMoveEvent(), hence
    // the following call to QCoreApplication::processEvents()
    QCoreApplication::processEvents();


    // Regarding 'move in Move' when
    // RecursingDropTarget::dragMoveEvent() runs,
    // its call 'evaluate' triggers a second
    // move event (x = 23) that needs to be delivered.
    QCoreApplication::processEvents();

    QCOMPARE(dropTarget.enterEvents, 1);
    QCOMPARE(dropTarget.moveEvents, moveEvents);
    QCOMPARE(dropTarget.dropEvents, 0);
    QCOMPARE(dropTarget.leaveEvents, 0);

    if (type == QEvent::Drop) {
        QCOMPARE(evaluate<bool>(item, "Drag.drop() == Qt.MoveAction"), true);
        QCOMPARE(dropTarget.enterEvents, 1);
        QCOMPARE(dropTarget.moveEvents, moveEvents);
        QCOMPARE(dropTarget.dropEvents, 1);
        QCOMPARE(dropTarget.leaveEvents, 0);
    } else {
        evaluate<void>(item, "Drag.cancel()");
        QCOMPARE(dropTarget.enterEvents, 1);
        QCOMPARE(dropTarget.moveEvents, moveEvents);
        QCOMPARE(dropTarget.dropEvents, 0);
        QCOMPARE(dropTarget.leaveEvents, 1);
    }
}

void tst_QQuickDrag::noCrashWithImageProvider()
{
    // QTBUG-72045
    QQmlComponent component(&engine);
    component.setData(
    R"(
    import QtQuick 2.9
    Item {
        Rectangle {
            id: item
            width: 50
            height: 50
            anchors.centerIn: parent
            color: "orange"
            Component.onCompleted: {
                item.Drag.imageSource = "image://kill/me"
            }
        }
    })", QUrl());
    QScopedPointer<QObject> object(component.create());
    QQuickItem *item = qobject_cast<QQuickItem *>(object.data());
    QVERIFY(item);
}


QTEST_MAIN(tst_QQuickDrag)

#include "tst_qquickdrag.moc"
