dnd: send DragEnter and DragMove on DnD start

This was a regression from Qt4 and also is the documented behavior.
In addition this patch fixes various issues with cursor shape updating
that were discovered along the way and that are necessary for testing
the new changes.

The code in QGuiApplicationPrivate::processDrag() also needed a fixup,
particularly the resetting of QGuiApplicationPrivate::currentDragWindow.
Without this fix we would get DragMove (the one that immediately follows
the DragEnter) only for the first DragEnter event. For example when dnd
starts on mouse press then for mouse click we would get:

<click> DragEnter->DragMove->DragLeave <click> DragEnter->DragLeave

but the expected is:

<click> DragEnter->DragMove->DragLeave <click> DragEnter->DragMove->DragLeave

Task-number: QTBUG-34331
Change-Id: I3cc96c87d1fd5d1342c7f6c9438802ab30076e9e
Reviewed-by: Shawn Rutledge <shawn.rutledge@qt.io>
This commit is contained in:
Gatis Paeglis 2018-05-22 16:33:53 +02:00
parent ca3460775c
commit 7a7c722782
8 changed files with 177 additions and 65 deletions

View File

@ -143,6 +143,8 @@ Qt::ApplicationState QGuiApplicationPrivate::applicationState = Qt::ApplicationI
bool QGuiApplicationPrivate::highDpiScalingUpdated = false; bool QGuiApplicationPrivate::highDpiScalingUpdated = false;
QPointer<QWindow> QGuiApplicationPrivate::currentDragWindow;
QVector<QGuiApplicationPrivate::TabletPointData> QGuiApplicationPrivate::tabletDevicePoints; QVector<QGuiApplicationPrivate::TabletPointData> QGuiApplicationPrivate::tabletDevicePoints;
QPlatformIntegration *QGuiApplicationPrivate::platform_integration = 0; QPlatformIntegration *QGuiApplicationPrivate::platform_integration = 0;
@ -668,6 +670,7 @@ QGuiApplication::~QGuiApplication()
QGuiApplicationPrivate::currentMousePressWindow = QGuiApplicationPrivate::currentMouseWindow = nullptr; QGuiApplicationPrivate::currentMousePressWindow = QGuiApplicationPrivate::currentMouseWindow = nullptr;
QGuiApplicationPrivate::applicationState = Qt::ApplicationInactive; QGuiApplicationPrivate::applicationState = Qt::ApplicationInactive;
QGuiApplicationPrivate::highDpiScalingUpdated = false; QGuiApplicationPrivate::highDpiScalingUpdated = false;
QGuiApplicationPrivate::currentDragWindow = nullptr;
QGuiApplicationPrivate::tabletDevicePoints.clear(); QGuiApplicationPrivate::tabletDevicePoints.clear();
#ifndef QT_NO_SESSIONMANAGER #ifndef QT_NO_SESSIONMANAGER
QGuiApplicationPrivate::is_fallback_session_management_enabled = true; QGuiApplicationPrivate::is_fallback_session_management_enabled = true;
@ -3092,7 +3095,6 @@ QPlatformDragQtResponse QGuiApplicationPrivate::processDrag(QWindow *w, const QM
{ {
updateMouseAndModifierButtonState(buttons, modifiers); updateMouseAndModifierButtonState(buttons, modifiers);
static QPointer<QWindow> currentDragWindow;
static Qt::DropAction lastAcceptedDropAction = Qt::IgnoreAction; static Qt::DropAction lastAcceptedDropAction = Qt::IgnoreAction;
QPlatformDrag *platformDrag = platformIntegration()->drag(); QPlatformDrag *platformDrag = platformIntegration()->drag();
if (!platformDrag || (w && w->d_func()->blockedByModalWindow)) { if (!platformDrag || (w && w->d_func()->blockedByModalWindow)) {
@ -3101,8 +3103,7 @@ QPlatformDragQtResponse QGuiApplicationPrivate::processDrag(QWindow *w, const QM
} }
if (!dropData) { if (!dropData) {
if (currentDragWindow.data() == w) currentDragWindow = nullptr;
currentDragWindow = 0;
QDragLeaveEvent e; QDragLeaveEvent e;
QGuiApplication::sendEvent(w, &e); QGuiApplication::sendEvent(w, &e);
lastAcceptedDropAction = Qt::IgnoreAction; lastAcceptedDropAction = Qt::IgnoreAction;
@ -3141,6 +3142,8 @@ QPlatformDropQtResponse QGuiApplicationPrivate::processDrop(QWindow *w, const QM
{ {
updateMouseAndModifierButtonState(buttons, modifiers); updateMouseAndModifierButtonState(buttons, modifiers);
currentDragWindow = nullptr;
QDropEvent de(p, supportedActions, dropData, buttons, modifiers); QDropEvent de(p, supportedActions, dropData, buttons, modifiers);
QGuiApplication::sendEvent(w, &de); QGuiApplication::sendEvent(w, &de);

View File

@ -217,6 +217,7 @@ public:
static QWindow *currentMousePressWindow; static QWindow *currentMousePressWindow;
static Qt::ApplicationState applicationState; static Qt::ApplicationState applicationState;
static bool highDpiScalingUpdated; static bool highDpiScalingUpdated;
static QPointer<QWindow> currentDragWindow;
struct TabletPointData { struct TabletPointData {
TabletPointData(qint64 devId = 0) : deviceId(devId), state(Qt::NoButton), target(nullptr) {} TabletPointData(qint64 devId = 0) : deviceId(devId), state(Qt::NoButton), target(nullptr) {}

View File

@ -99,8 +99,8 @@ QPlatformClipboard *QPlatformIntegration::clipboard() const
/*! /*!
Accessor for the platform integration's drag object. Accessor for the platform integration's drag object.
Default implementation returns 0, implying no drag and drop support. Default implementation returns QSimpleDrag. This class supports only drag
and drop operations within the same Qt application.
*/ */
QPlatformDrag *QPlatformIntegration::drag() const QPlatformDrag *QPlatformIntegration::drag() const
{ {

View File

@ -94,11 +94,7 @@ static QWindow* topLevelAt(const QPoint &pos)
(within the Qt application or outside) accepts the drag and sets the state accordingly. (within the Qt application or outside) accepts the drag and sets the state accordingly.
*/ */
QBasicDrag::QBasicDrag() : QBasicDrag::QBasicDrag()
m_current_window(nullptr), m_restoreCursor(false), m_eventLoop(nullptr),
m_executed_drop_action(Qt::IgnoreAction), m_can_drop(false),
m_drag(nullptr), m_drag_icon_window(nullptr), m_useCompositing(true),
m_screen(nullptr)
{ {
} }
@ -181,9 +177,9 @@ bool QBasicDrag::eventFilter(QObject *o, QEvent *e)
// make the event relative to the window where the drag started. (QTBUG-66103) // make the event relative to the window where the drag started. (QTBUG-66103)
const QMouseEvent *release = static_cast<QMouseEvent *>(e); const QMouseEvent *release = static_cast<QMouseEvent *>(e);
const QWindow *releaseWindow = topLevelAt(release->globalPos()); const QWindow *releaseWindow = topLevelAt(release->globalPos());
qCDebug(lcDnd) << "mouse released over" << releaseWindow << "after drag from" << m_current_window << "globalPos" << release->globalPos(); qCDebug(lcDnd) << "mouse released over" << releaseWindow << "after drag from" << m_sourceWindow << "globalPos" << release->globalPos();
if (!releaseWindow) if (!releaseWindow)
releaseWindow = m_current_window; releaseWindow = m_sourceWindow;
QPoint releaseWindowPos = (releaseWindow ? releaseWindow->mapFromGlobal(release->globalPos()) : release->globalPos()); QPoint releaseWindowPos = (releaseWindow ? releaseWindow->mapFromGlobal(release->globalPos()) : release->globalPos());
QMouseEvent *newRelease = new QMouseEvent(release->type(), QMouseEvent *newRelease = new QMouseEvent(release->type(),
releaseWindowPos, releaseWindowPos, release->screenPos(), releaseWindowPos, releaseWindowPos, release->screenPos(),
@ -206,18 +202,15 @@ Qt::DropAction QBasicDrag::drag(QDrag *o)
m_drag = o; m_drag = o;
m_executed_drop_action = Qt::IgnoreAction; m_executed_drop_action = Qt::IgnoreAction;
m_can_drop = false; m_can_drop = false;
m_restoreCursor = true;
#ifndef QT_NO_CURSOR
qApp->setOverrideCursor(Qt::DragCopyCursor);
updateCursor(m_executed_drop_action);
#endif
startDrag(); startDrag();
m_eventLoop = new QEventLoop; m_eventLoop = new QEventLoop;
m_eventLoop->exec(); m_eventLoop->exec();
delete m_eventLoop; delete m_eventLoop;
m_eventLoop = 0; m_eventLoop = nullptr;
m_drag = 0; m_drag = nullptr;
endDrag(); endDrag();
return m_executed_drop_action; return m_executed_drop_action;
} }
@ -229,16 +222,6 @@ void QBasicDrag::cancelDrag()
} }
} }
void QBasicDrag::restoreCursor()
{
if (m_restoreCursor) {
#ifndef QT_NO_CURSOR
QGuiApplication::restoreOverrideCursor();
#endif
m_restoreCursor = false;
}
}
void QBasicDrag::startDrag() void QBasicDrag::startDrag()
{ {
QPoint pos; QPoint pos;
@ -320,25 +303,34 @@ void QBasicDrag::updateCursor(Qt::DropAction action)
} }
} }
QCursor *cursor = QGuiApplication::overrideCursor();
QPixmap pixmap = m_drag->dragCursor(action); QPixmap pixmap = m_drag->dragCursor(action);
if (!cursor) {
QGuiApplication::changeOverrideCursor((pixmap.isNull()) ? QCursor(cursorShape) : QCursor(pixmap)); if (!m_dndHasSetOverrideCursor) {
QCursor newCursor = !pixmap.isNull() ? QCursor(pixmap) : QCursor(cursorShape);
QGuiApplication::setOverrideCursor(newCursor);
m_dndHasSetOverrideCursor = true;
} else { } else {
QCursor *cursor = QGuiApplication::overrideCursor();
if (!pixmap.isNull()) { if (!pixmap.isNull()) {
if ((cursor->pixmap().cacheKey() != pixmap.cacheKey())) { if (cursor->pixmap().cacheKey() != pixmap.cacheKey())
QGuiApplication::changeOverrideCursor(QCursor(pixmap)); QGuiApplication::changeOverrideCursor(QCursor(pixmap));
} } else if (cursorShape != cursor->shape()) {
} else {
if (cursorShape != cursor->shape()) {
QGuiApplication::changeOverrideCursor(QCursor(cursorShape)); QGuiApplication::changeOverrideCursor(QCursor(cursorShape));
} }
} }
}
#endif #endif
updateAction(action); updateAction(action);
} }
void QBasicDrag::restoreCursor()
{
#ifndef QT_NO_CURSOR
if (m_dndHasSetOverrideCursor) {
QGuiApplication::restoreOverrideCursor();
m_dndHasSetOverrideCursor = false;
}
#endif
}
static inline QPoint fromNativeGlobalPixels(const QPoint &point) static inline QPoint fromNativeGlobalPixels(const QPoint &point)
{ {
@ -376,35 +368,38 @@ QSimpleDrag::QSimpleDrag()
void QSimpleDrag::startDrag() void QSimpleDrag::startDrag()
{ {
setExecutedDropAction(Qt::IgnoreAction);
QBasicDrag::startDrag(); QBasicDrag::startDrag();
// Here we can be fairly sure that QGuiApplication::mouseButtons/keyboardModifiers() will // Here we can be fairly sure that QGuiApplication::mouseButtons/keyboardModifiers() will
// contain sensible values as startDrag() normally is called from mouse event handlers // contain sensible values as startDrag() normally is called from mouse event handlers
// by QDrag::exec(). A better API would be if we could pass something like "input device // by QDrag::exec(). A better API would be if we could pass something like "input device
// pointer" to QDrag::exec(). My guess is that something like that might be required for // pointer" to QDrag::exec(). My guess is that something like that might be required for
// QTBUG-52430. // QTBUG-52430.
m_current_window = topLevelAt(QCursor::pos()); m_sourceWindow = topLevelAt(QCursor::pos());
if (m_current_window) { m_windowUnderCursor = m_sourceWindow;
auto nativePixelPos = QHighDpi::toNativePixels(QCursor::pos(), m_current_window); if (m_sourceWindow) {
QPlatformDragQtResponse response = QWindowSystemInterface::handleDrag( auto nativePixelPos = QHighDpi::toNativePixels(QCursor::pos(), m_sourceWindow);
m_current_window, drag()->mimeData(), nativePixelPos, move(nativePixelPos, QGuiApplication::mouseButtons(), QGuiApplication::keyboardModifiers());
drag()->supportedActions(), QGuiApplication::mouseButtons(),
QGuiApplication::keyboardModifiers());
setCanDrop(response.isAccepted());
updateCursor(response.acceptedAction());
} else { } else {
setCanDrop(false); setCanDrop(false);
updateCursor(Qt::IgnoreAction); updateCursor(Qt::IgnoreAction);
} }
setExecutedDropAction(Qt::IgnoreAction);
qCDebug(lcDnd) << "drag began from" << m_current_window<< "cursor pos" << QCursor::pos() << "can drop?" << canDrop(); qCDebug(lcDnd) << "drag began from" << m_sourceWindow << "cursor pos" << QCursor::pos() << "can drop?" << canDrop();
}
static void sendDragLeave(QWindow *window)
{
QWindowSystemInterface::handleDrag(window, nullptr, QPoint(), Qt::IgnoreAction, 0, 0);
} }
void QSimpleDrag::cancel() void QSimpleDrag::cancel()
{ {
QBasicDrag::cancel(); QBasicDrag::cancel();
if (drag() && m_current_window) { if (drag() && m_sourceWindow) {
QWindowSystemInterface::handleDrag(m_current_window, nullptr, QPoint(), Qt::IgnoreAction, 0, 0); sendDragLeave(m_sourceWindow);
m_current_window = nullptr; m_sourceWindow = nullptr;
} }
} }
@ -414,16 +409,26 @@ void QSimpleDrag::move(const QPoint &nativeGlobalPos, Qt::MouseButtons buttons,
QPoint globalPos = fromNativeGlobalPixels(nativeGlobalPos); QPoint globalPos = fromNativeGlobalPixels(nativeGlobalPos);
moveShapedPixmapWindow(globalPos); moveShapedPixmapWindow(globalPos);
QWindow *window = topLevelAt(globalPos); QWindow *window = topLevelAt(globalPos);
if (!window)
if (!window || window != m_windowUnderCursor) {
if (m_windowUnderCursor)
sendDragLeave(m_windowUnderCursor);
m_windowUnderCursor = window;
if (!window) {
// QSimpleDrag supports only in-process dnd, we can't drop anywhere else.
setCanDrop(false);
updateCursor(Qt::IgnoreAction);
return; return;
}
}
const QPoint pos = nativeGlobalPos - window->handle()->geometry().topLeft(); const QPoint pos = nativeGlobalPos - window->handle()->geometry().topLeft();
const QPlatformDragQtResponse qt_response = QWindowSystemInterface::handleDrag( const QPlatformDragQtResponse qt_response = QWindowSystemInterface::handleDrag(
window, drag()->mimeData(), pos, drag()->supportedActions(), window, drag()->mimeData(), pos, drag()->supportedActions(),
buttons, modifiers); buttons, modifiers);
updateCursor(qt_response.acceptedAction());
setCanDrop(qt_response.isAccepted()); setCanDrop(qt_response.isAccepted());
updateCursor(qt_response.acceptedAction());
} }
void QSimpleDrag::drop(const QPoint &nativeGlobalPos, Qt::MouseButtons buttons, void QSimpleDrag::drop(const QPoint &nativeGlobalPos, Qt::MouseButtons buttons,

View File

@ -55,13 +55,14 @@
#include <qpa/qplatformdrag.h> #include <qpa/qplatformdrag.h>
#include <QtCore/QObject> #include <QtCore/QObject>
#include <QtCore/QPointer>
#include <QtGui/QWindow>
QT_REQUIRE_CONFIG(draganddrop); QT_REQUIRE_CONFIG(draganddrop);
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
class QMouseEvent; class QMouseEvent;
class QWindow;
class QEventLoop; class QEventLoop;
class QDropData; class QDropData;
class QShapedPixmapWindow; class QShapedPixmapWindow;
@ -106,7 +107,8 @@ protected:
QDrag *drag() const { return m_drag; } QDrag *drag() const { return m_drag; }
protected: protected:
QWindow *m_current_window; QWindow *m_sourceWindow = nullptr;
QPointer<QWindow> m_windowUnderCursor = nullptr;
private: private:
void enableEventFilter(); void enableEventFilter();
@ -114,14 +116,16 @@ private:
void restoreCursor(); void restoreCursor();
void exitDndEventLoop(); void exitDndEventLoop();
bool m_restoreCursor; #ifndef QT_NO_CURSOR
QEventLoop *m_eventLoop; bool m_dndHasSetOverrideCursor = false;
Qt::DropAction m_executed_drop_action; #endif
bool m_can_drop; QEventLoop *m_eventLoop = nullptr;
QDrag *m_drag; Qt::DropAction m_executed_drop_action = Qt::IgnoreAction;
QShapedPixmapWindow *m_drag_icon_window; bool m_can_drop = false;
bool m_useCompositing; QDrag *m_drag = nullptr;
QScreen *m_screen; QShapedPixmapWindow *m_drag_icon_window = nullptr;
bool m_useCompositing = true;
QScreen *m_screen = nullptr;
}; };
class Q_GUI_EXPORT QSimpleDrag : public QBasicDrag class Q_GUI_EXPORT QSimpleDrag : public QBasicDrag

View File

@ -201,6 +201,9 @@ void QXcbDrag::startDrag()
QBasicDrag::startDrag(); QBasicDrag::startDrag();
if (connection()->mouseGrabber() == nullptr) if (connection()->mouseGrabber() == nullptr)
shapedPixmapWindow()->setMouseGrabEnabled(true); shapedPixmapWindow()->setMouseGrabEnabled(true);
auto nativePixelPos = QHighDpi::toNativePixels(QCursor::pos(), initiatorWindow);
move(nativePixelPos, QGuiApplication::mouseButtons(), QGuiApplication::keyboardModifiers());
} }
void QXcbDrag::endDrag() void QXcbDrag::endDrag()

View File

@ -379,8 +379,17 @@ QPlatformClipboard *QXcbIntegration::clipboard() const
#endif #endif
#if QT_CONFIG(draganddrop) #if QT_CONFIG(draganddrop)
#include <private/qsimpledrag_p.h>
QPlatformDrag *QXcbIntegration::drag() const QPlatformDrag *QXcbIntegration::drag() const
{ {
static const bool useSimpleDrag = qEnvironmentVariableIsSet("QT_XCB_USE_SIMPLE_DRAG");
if (Q_UNLIKELY(useSimpleDrag)) { // This is useful for testing purposes
static QSimpleDrag *simpleDrag = nullptr;
if (!simpleDrag)
simpleDrag = new QSimpleDrag();
return simpleDrag;
}
return m_connections.at(0)->drag(); return m_connections.at(0)->drag();
} }
#endif #endif

View File

@ -83,6 +83,7 @@ private slots:
#if QT_CONFIG(draganddrop) #if QT_CONFIG(draganddrop)
void tst_dnd(); void tst_dnd();
void tst_dnd_events();
#endif #endif
void tst_qtbug35600(); void tst_qtbug35600();
@ -597,6 +598,92 @@ void tst_QWidget_window::tst_dnd()
QCOMPARE(log, expectedLog); QCOMPARE(log, expectedLog);
} }
class DnDEventRecorder : public QWidget
{
Q_OBJECT
public:
QString _dndEvents;
DnDEventRecorder() { setAcceptDrops(true); }
protected:
void mousePressEvent(QMouseEvent *)
{
QMimeData *mimeData = new QMimeData;
mimeData->setData("application/x-dnditemdata", "some data");
QDrag *drag = new QDrag(this);
drag->setMimeData(mimeData);
drag->exec();
}
void dragEnterEvent(QDragEnterEvent *e)
{
e->accept();
_dndEvents.append(QStringLiteral("DragEnter "));
}
void dragMoveEvent(QDragMoveEvent *e)
{
e->accept();
_dndEvents.append(QStringLiteral("DragMove "));
emit releaseMouseButton();
}
void dragLeaveEvent(QDragLeaveEvent *e)
{
e->accept();
_dndEvents.append(QStringLiteral("DragLeave "));
}
void dropEvent(QDropEvent *e)
{
e->accept();
_dndEvents.append(QStringLiteral("DropEvent "));
}
signals:
void releaseMouseButton();
};
void tst_QWidget_window::tst_dnd_events()
{
// Note: This test is somewhat a hack as testing DnD with qtestlib is not
// supported at the moment. The test verifies that we get an expected event
// sequence on dnd operation that does not move a mouse. This logic is implemented
// in QGuiApplication, so we have to go via QWindowSystemInterface API (QTest::mouse*).
const auto platformName = QGuiApplication::platformName().toLower();
// The test is known to work with XCB and platforms that use the default dnd
// implementation QSimpleDrag (e.g. qnx). Running on XCB should be sufficient to
// catch regressions at cross platform code: QGuiApplication::processDrag/Leave().
if (platformName != "xcb")
return;
const QString expectedDndEvents = "DragEnter DragMove DropEvent DragEnter DragMove "
"DropEvent DragEnter DragMove DropEvent ";
DnDEventRecorder dndWidget;
dndWidget.setGeometry(100, 100, 200, 200);
dndWidget.show();
QVERIFY(QTest::qWaitForWindowExposed(&dndWidget));
QVERIFY(QTest::qWaitForWindowActive(&dndWidget));
// ### FIXME - QTBUG-35117 ???
auto targetCenter = QPoint(dndWidget.width(), dndWidget.height()) / 2;
auto targetCenterGlobal = dndWidget.mapToGlobal(targetCenter);
QCursor::setPos(targetCenterGlobal);
QVERIFY(QTest::qWaitFor([&]() { return QCursor::pos() == targetCenterGlobal; }));
QCoreApplication::processEvents(); // clear mouse events generated from cursor
auto window = dndWidget.window()->windowHandle();
// Some dnd implementation rely on running internal event loops, so we have to use
// the following queued signal hack to simulate mouse clicks in the widget.
QObject::connect(&dndWidget, &DnDEventRecorder::releaseMouseButton, this, [=]() {
QTest::mouseRelease(window, Qt::LeftButton);
}, Qt::QueuedConnection);
QTest::mousePress(window, Qt::LeftButton);
QTest::mousePress(window, Qt::LeftButton);
QTest::mousePress(window, Qt::LeftButton);
QCOMPARE(dndWidget._dndEvents, expectedDndEvents);
}
#endif #endif
void tst_QWidget_window::tst_qtbug35600() void tst_QWidget_window::tst_qtbug35600()