1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qsimpledrag_p.h"
5
6#include "qbitmap.h"
7#include "qdrag.h"
8#include "qpixmap.h"
9#include "qevent.h"
10#include "qfile.h"
11#include "qguiapplication.h"
12#include "qpoint.h"
13#include "qbuffer.h"
14#include "qimage.h"
15#include "qdir.h"
16#include "qimagereader.h"
17#include "qimagewriter.h"
18#include "qplatformscreen.h"
19#include "qplatformwindow.h"
20
21#include <QtCore/QEventLoop>
22#include <QtCore/QDebug>
23#include <QtCore/QLoggingCategory>
24
25#include <private/qguiapplication_p.h>
26#include <private/qdnd_p.h>
27
28#include <private/qshapedpixmapdndwindow_p.h>
29#include <private/qhighdpiscaling_p.h>
30
31QT_BEGIN_NAMESPACE
32
33Q_LOGGING_CATEGORY(lcDnd, "qt.gui.dnd")
34
35static QWindow* topLevelAt(const QPoint &pos)
36{
37 const QWindowList list = QGuiApplication::topLevelWindows();
38 const auto crend = list.crend();
39 for (auto it = list.crbegin(); it != crend; ++it) {
40 QWindow *w = *it;
41 if (w->isVisible() && w->handle() && w->geometry().contains(p: pos) && !qobject_cast<QShapedPixmapWindow*>(object: w))
42 return w;
43 }
44 return nullptr;
45}
46
47/*!
48 \class QBasicDrag
49 \brief QBasicDrag is a base class for implementing platform drag and drop.
50 \since 5.0
51 \internal
52 \ingroup qpa
53
54 QBasicDrag implements QPlatformDrag::drag() by running a local event loop in which
55 it tracks mouse movements and moves the drag icon (QShapedPixmapWindow) accordingly.
56 It provides new virtuals allowing for querying whether the receiving window
57 (within the Qt application or outside) accepts the drag and sets the state accordingly.
58*/
59
60QBasicDrag::QBasicDrag()
61{
62}
63
64QBasicDrag::~QBasicDrag()
65{
66 delete m_drag_icon_window;
67}
68
69void QBasicDrag::enableEventFilter()
70{
71 qApp->installEventFilter(filterObj: this);
72}
73
74void QBasicDrag::disableEventFilter()
75{
76 qApp->removeEventFilter(obj: this);
77}
78
79
80static inline QPoint getNativeMousePos(QEvent *e, QWindow *window)
81{
82 return QHighDpi::toNativePixels(value: static_cast<QMouseEvent *>(e)->globalPosition().toPoint(), context: window);
83}
84
85bool QBasicDrag::eventFilter(QObject *o, QEvent *e)
86{
87 Q_UNUSED(o);
88
89 if (!m_drag) {
90 if (e->type() == QEvent::KeyRelease && static_cast<QKeyEvent*>(e)->key() == Qt::Key_Escape) {
91 disableEventFilter();
92 exitDndEventLoop();
93 return true; // block the key release
94 }
95 return false;
96 }
97
98 switch (e->type()) {
99 case QEvent::ShortcutOverride:
100 // prevent accelerators from firing while dragging
101 e->accept();
102 return true;
103
104 case QEvent::KeyPress:
105 case QEvent::KeyRelease:
106 {
107 QKeyEvent *ke = static_cast<QKeyEvent *>(e);
108 if (ke->key() == Qt::Key_Escape && e->type() == QEvent::KeyPress) {
109 cancel();
110 disableEventFilter();
111 exitDndEventLoop();
112
113 } else if (ke->modifiers() != QGuiApplication::keyboardModifiers()) {
114 move(globalPos: m_lastPos, b: QGuiApplication::mouseButtons(), mods: ke->modifiers());
115 }
116 return true; // Eat all key events
117 }
118
119 case QEvent::MouseMove:
120 {
121 m_lastPos = getNativeMousePos(e, window: m_drag_icon_window);
122 auto mouseMove = static_cast<QMouseEvent *>(e);
123 move(globalPos: m_lastPos, b: mouseMove->buttons(), mods: mouseMove->modifiers());
124 return true; // Eat all mouse move events
125 }
126 case QEvent::MouseButtonRelease:
127 {
128 QPointer<QObject> objGuard(o);
129 disableEventFilter();
130 if (canDrop()) {
131 QPoint nativePosition = getNativeMousePos(e, window: m_drag_icon_window);
132 auto mouseRelease = static_cast<QMouseEvent *>(e);
133 drop(globalPos: nativePosition, b: mouseRelease->buttons(), mods: mouseRelease->modifiers());
134 } else {
135 cancel();
136 }
137 exitDndEventLoop();
138 if (!objGuard)
139 return true;
140
141 // If a QShapedPixmapWindow (drag feedback) is being dragged along, the
142 // mouse event's localPos() will be relative to that, which is useless.
143 // We want a position relative to the window where the drag ends, if possible (?).
144 // If there is no such window (belonging to this Qt application),
145 // make the event relative to the window where the drag started. (QTBUG-66103)
146 const QMouseEvent *release = static_cast<QMouseEvent *>(e);
147 const QWindow *releaseWindow = topLevelAt(pos: release->globalPosition().toPoint());
148 qCDebug(lcDnd) << "mouse released over" << releaseWindow << "after drag from" << m_sourceWindow << "globalPos" << release->globalPosition().toPoint();
149 if (!releaseWindow)
150 releaseWindow = m_sourceWindow;
151 QPointF releaseWindowPos = (releaseWindow ? releaseWindow->mapFromGlobal(pos: release->globalPosition()) : release->globalPosition());
152 QMouseEvent *newRelease = new QMouseEvent(release->type(),
153 releaseWindowPos, releaseWindowPos, release->globalPosition(),
154 release->button(), release->buttons(),
155 release->modifiers(), release->source(), release->pointingDevice());
156 QCoreApplication::postEvent(receiver: o, event: newRelease);
157 return true; // defer mouse release events until drag event loop has returned
158 }
159 case QEvent::MouseButtonDblClick:
160 case QEvent::Wheel:
161 return true;
162 default:
163 break;
164 }
165 return false;
166}
167
168Qt::DropAction QBasicDrag::drag(QDrag *o)
169{
170 m_drag = o;
171 m_executed_drop_action = Qt::IgnoreAction;
172 m_can_drop = false;
173
174 startDrag();
175 m_eventLoop = new QEventLoop;
176 m_eventLoop->exec();
177 delete m_eventLoop;
178 m_eventLoop = nullptr;
179 m_drag = nullptr;
180 endDrag();
181
182 return m_executed_drop_action;
183}
184
185void QBasicDrag::cancelDrag()
186{
187 if (m_eventLoop) {
188 cancel();
189 m_eventLoop->quit();
190 }
191}
192
193void QBasicDrag::startDrag()
194{
195 QPoint pos;
196#ifndef QT_NO_CURSOR
197 pos = QCursor::pos();
198 if (pos.x() == int(qInf())) {
199 // ### fixme: no mouse pos registered. Get pos from touch...
200 pos = QPoint();
201 }
202#endif
203 m_lastPos = pos;
204 recreateShapedPixmapWindow(screen: m_screen, pos);
205 enableEventFilter();
206}
207
208void QBasicDrag::endDrag()
209{
210}
211
212void QBasicDrag::recreateShapedPixmapWindow(QScreen *screen, const QPoint &pos)
213{
214 delete m_drag_icon_window;
215 // ### TODO Check if its really necessary to have m_drag_icon_window
216 // when QDrag is used without a pixmap - QDrag::setPixmap()
217 m_drag_icon_window = new QShapedPixmapWindow(screen);
218
219 m_drag_icon_window->setUseCompositing(m_useCompositing);
220 m_drag_icon_window->setPixmap(m_drag->pixmap());
221 m_drag_icon_window->setHotspot(m_drag->hotSpot());
222 m_drag_icon_window->updateGeometry(pos);
223 m_drag_icon_window->setVisible(true);
224}
225
226void QBasicDrag::cancel()
227{
228 disableEventFilter();
229 restoreCursor();
230 m_drag_icon_window->setVisible(false);
231}
232
233/*!
234 Move the drag label to \a globalPos, which is
235 interpreted in device independent coordinates. Typically called from reimplementations of move().
236 */
237
238void QBasicDrag::moveShapedPixmapWindow(const QPoint &globalPos)
239{
240 if (m_drag)
241 m_drag_icon_window->updateGeometry(pos: globalPos);
242}
243
244void QBasicDrag::drop(const QPoint &, Qt::MouseButtons, Qt::KeyboardModifiers)
245{
246 disableEventFilter();
247 restoreCursor();
248 m_drag_icon_window->setVisible(false);
249}
250
251void QBasicDrag::exitDndEventLoop()
252{
253 if (m_eventLoop && m_eventLoop->isRunning())
254 m_eventLoop->exit();
255}
256
257void QBasicDrag::updateCursor(Qt::DropAction action)
258{
259#ifndef QT_NO_CURSOR
260 Qt::CursorShape cursorShape = Qt::ForbiddenCursor;
261 if (canDrop()) {
262 switch (action) {
263 case Qt::CopyAction:
264 cursorShape = Qt::DragCopyCursor;
265 break;
266 case Qt::LinkAction:
267 cursorShape = Qt::DragLinkCursor;
268 break;
269 default:
270 cursorShape = Qt::DragMoveCursor;
271 break;
272 }
273 }
274
275 QPixmap pixmap = m_drag->dragCursor(action);
276
277 if (!m_dndHasSetOverrideCursor) {
278 QCursor newCursor = !pixmap.isNull() ? QCursor(pixmap) : QCursor(cursorShape);
279 QGuiApplication::setOverrideCursor(newCursor);
280 m_dndHasSetOverrideCursor = true;
281 } else {
282 QCursor *cursor = QGuiApplication::overrideCursor();
283 if (!cursor) {
284 QGuiApplication::changeOverrideCursor(pixmap.isNull() ? QCursor(cursorShape) : QCursor(pixmap));
285 } else {
286 if (!pixmap.isNull()) {
287 if (cursor->pixmap().cacheKey() != pixmap.cacheKey())
288 QGuiApplication::changeOverrideCursor(QCursor(pixmap));
289 } else if (cursorShape != cursor->shape()) {
290 QGuiApplication::changeOverrideCursor(QCursor(cursorShape));
291 }
292 }
293 }
294#endif
295 updateAction(action);
296}
297
298void QBasicDrag::restoreCursor()
299{
300#ifndef QT_NO_CURSOR
301 if (m_dndHasSetOverrideCursor) {
302 QGuiApplication::restoreOverrideCursor();
303 m_dndHasSetOverrideCursor = false;
304 }
305#endif
306}
307
308static inline QPoint fromNativeGlobalPixels(const QPoint &point)
309{
310#ifndef QT_NO_HIGHDPISCALING
311 QPoint res = point;
312 if (QHighDpiScaling::isActive()) {
313 for (const QScreen *s : std::as_const(t&: QGuiApplicationPrivate::screen_list)) {
314 if (s->handle()->geometry().contains(p: point)) {
315 res = QHighDpi::fromNativePixels(value: point, context: s);
316 break;
317 }
318 }
319 }
320 return res;
321#else
322 return point;
323#endif
324}
325
326/*!
327 \class QSimpleDrag
328 \brief QSimpleDrag implements QBasicDrag for Drag and Drop operations within the Qt Application itself.
329 \since 5.0
330 \internal
331 \ingroup qpa
332
333 The class checks whether the receiving window is a window of the Qt application
334 and sets the state accordingly. It does not take windows of other applications
335 into account.
336*/
337
338QSimpleDrag::QSimpleDrag()
339{
340}
341
342void QSimpleDrag::startDrag()
343{
344 setExecutedDropAction(Qt::IgnoreAction);
345
346 QBasicDrag::startDrag();
347 // Here we can be fairly sure that QGuiApplication::mouseButtons/keyboardModifiers() will
348 // contain sensible values as startDrag() normally is called from mouse event handlers
349 // by QDrag::exec(). A better API would be if we could pass something like "input device
350 // pointer" to QDrag::exec(). My guess is that something like that might be required for
351 // QTBUG-52430.
352 m_sourceWindow = topLevelAt(pos: QCursor::pos());
353 m_windowUnderCursor = m_sourceWindow;
354 if (m_sourceWindow) {
355 auto nativePixelPos = QHighDpi::toNativePixels(value: QCursor::pos(), context: m_sourceWindow);
356 move(globalPos: nativePixelPos, b: QGuiApplication::mouseButtons(), mods: QGuiApplication::keyboardModifiers());
357 } else {
358 setCanDrop(false);
359 updateCursor(action: Qt::IgnoreAction);
360 }
361
362 qCDebug(lcDnd) << "drag began from" << m_sourceWindow << "cursor pos" << QCursor::pos() << "can drop?" << canDrop();
363}
364
365static void sendDragLeave(QWindow *window)
366{
367 QWindowSystemInterface::handleDrag(window, dropData: nullptr, p: QPoint(), supportedActions: Qt::IgnoreAction, buttons: { }, modifiers: { });
368}
369
370void QSimpleDrag::cancel()
371{
372 QBasicDrag::cancel();
373 if (drag() && m_sourceWindow) {
374 sendDragLeave(window: m_sourceWindow);
375 m_sourceWindow = nullptr;
376 }
377}
378
379void QSimpleDrag::move(const QPoint &nativeGlobalPos, Qt::MouseButtons buttons,
380 Qt::KeyboardModifiers modifiers)
381{
382 QPoint globalPos = fromNativeGlobalPixels(point: nativeGlobalPos);
383 moveShapedPixmapWindow(globalPos);
384 QWindow *window = topLevelAt(pos: globalPos);
385
386 if (!window || window != m_windowUnderCursor) {
387 if (m_windowUnderCursor)
388 sendDragLeave(window: m_windowUnderCursor);
389 m_windowUnderCursor = window;
390 if (!window) {
391 // QSimpleDrag supports only in-process dnd, we can't drop anywhere else.
392 setCanDrop(false);
393 updateCursor(action: Qt::IgnoreAction);
394 return;
395 }
396 }
397
398 const QPoint pos = nativeGlobalPos - window->handle()->geometry().topLeft();
399 const QPlatformDragQtResponse qt_response = QWindowSystemInterface::handleDrag(
400 window, dropData: drag()->mimeData(), p: pos, supportedActions: drag()->supportedActions(),
401 buttons, modifiers);
402
403 setCanDrop(qt_response.isAccepted());
404 updateCursor(action: qt_response.acceptedAction());
405}
406
407void QSimpleDrag::drop(const QPoint &nativeGlobalPos, Qt::MouseButtons buttons,
408 Qt::KeyboardModifiers modifiers)
409{
410 QPoint globalPos = fromNativeGlobalPixels(point: nativeGlobalPos);
411
412 QBasicDrag::drop(nativeGlobalPos, buttons, modifiers);
413 QWindow *window = topLevelAt(pos: globalPos);
414 if (!window)
415 return;
416
417 const QPoint pos = nativeGlobalPos - window->handle()->geometry().topLeft();
418 const QPlatformDropQtResponse response = QWindowSystemInterface::handleDrop(
419 window, dropData: drag()->mimeData(), p: pos, supportedActions: drag()->supportedActions(),
420 buttons, modifiers);
421 if (response.isAccepted()) {
422 setExecutedDropAction(response.acceptedAction());
423 } else {
424 setExecutedDropAction(Qt::IgnoreAction);
425 }
426}
427
428QT_END_NAMESPACE
429

source code of qtbase/src/gui/kernel/qsimpledrag.cpp